Completed
Push — master ( 49e377...c5f217 )
by Frank
9s
created

AwsS3Adapter::getMetadata()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 25
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 13
cts 13
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 13
nc 3
nop 1
crap 4
1
<?php
2
3
namespace League\Flysystem\AwsS3v3;
4
5
use Aws\Result;
6
use Aws\S3\Exception\DeleteMultipleObjectsException;
7
use Aws\S3\Exception\S3Exception;
8
use Aws\S3\S3Client;
9
use League\Flysystem\Adapter\AbstractAdapter;
10
use League\Flysystem\AdapterInterface;
11
use League\Flysystem\Config;
12
use League\Flysystem\Util;
13
14
class AwsS3Adapter extends AbstractAdapter
15
{
16
    const PUBLIC_GRANT_URI = 'http://acs.amazonaws.com/groups/global/AllUsers';
17
18
    /**
19
     * @var array
20
     */
21
    protected static $resultMap = [
22
        'Body' => 'contents',
23
        'ContentLength' => 'size',
24
        'ContentType' => 'mimetype',
25
        'Size' => 'size',
26
    ];
27
28
    /**
29
     * @var array
30
     */
31
    protected static $metaOptions = [
32
        'CacheControl',
33
        'Expires',
34
        'StorageClass',
35
        'ServerSideEncryption',
36
        'Metadata',
37
        'ACL',
38
        'ContentType',
39
        'ContentEncoding',
40
        'ContentDisposition',
41
        'ContentLength',
42
        'WebsiteRedirectLocation'
43
    ];
44
45
    /**
46
     * @var S3Client
47
     */
48
    protected $s3Client;
49
50
    /**
51
     * @var string
52
     */
53
    protected $bucket;
54
55
    /**
56
     * @var array
57
     */
58
    protected $options = [];
59
60
    /**
61
     * Constructor.
62
     *
63
     * @param S3Client $client
64
     * @param string   $bucket
65
     * @param string   $prefix
66
     * @param array    $options
67
     */
68 72
    public function __construct(S3Client $client, $bucket, $prefix = '', array $options = [])
69
    {
70 72
        $this->s3Client = $client;
71 72
        $this->bucket = $bucket;
72 72
        $this->setPathPrefix($prefix);
73 72
        $this->options = $options;
74 72
    }
75
76
    /**
77
     * Get the S3Client bucket.
78
     *
79
     * @return string
80
     */
81 4
    public function getBucket()
82
    {
83 4
        return $this->bucket;
84
    }
85
86
    /**
87
     * Set the S3Client bucket.
88
     *
89
     * @return string
90
     */
91 2
    public function setBucket($bucket)
92
    {
93 2
        $this->bucket =  $bucket;
94 2
    }
95
96
    /**
97
     * Get the S3Client instance.
98
     *
99
     * @return S3Client
100
     */
101 2
    public function getClient()
102
    {
103 2
        return $this->s3Client;
104
    }
105
106
    /**
107
     * Write a new file.
108
     *
109
     * @param string $path
110
     * @param string $contents
111
     * @param Config $config Config object
112
     *
113
     * @return false|array false on failure file meta data on success
114
     */
115 2
    public function write($path, $contents, Config $config)
116
    {
117 2
        return $this->upload($path, $contents, $config);
118
    }
119
120
    /**
121
     * Update a file.
122
     *
123
     * @param string $path
124
     * @param string $contents
125
     * @param Config $config Config object
126
     *
127
     * @return false|array false on failure file meta data on success
128
     */
129 2
    public function update($path, $contents, Config $config)
130
    {
131 2
        return $this->upload($path, $contents, $config);
132
    }
133
134
    /**
135
     * Rename a file.
136
     *
137
     * @param string $path
138
     * @param string $newpath
139
     *
140
     * @return bool
141
     */
142 4
    public function rename($path, $newpath)
143
    {
144 4
        if ( ! $this->copy($path, $newpath)) {
145 2
            return false;
146
        }
147
148 2
        return $this->delete($path);
149
    }
150
151
    /**
152
     * Delete a file.
153
     *
154
     * @param string $path
155
     *
156
     * @return bool
157
     */
158 4
    public function delete($path)
159
    {
160 4
        $location = $this->applyPathPrefix($path);
161
162 4
        $command = $this->s3Client->getCommand(
163 4
            'deleteObject',
164
            [
165 4
                'Bucket' => $this->bucket,
166 4
                'Key' => $location,
167
            ]
168 4
        );
169
170 4
        $this->s3Client->execute($command);
171
172 4
        return ! $this->has($path);
173
    }
174
175
    /**
176
     * Delete a directory.
177
     *
178
     * @param string $dirname
179
     *
180
     * @return bool
181
     */
182 4
    public function deleteDir($dirname)
183
    {
184
        try {
185 4
            $prefix = $this->applyPathPrefix($dirname) . '/';
186 4
            $this->s3Client->deleteMatchingObjects($this->bucket, $prefix);
187 4
        } catch (DeleteMultipleObjectsException $exception) {
188 2
            return false;
189
        }
190
191 2
        return true;
192
    }
193
194
    /**
195
     * Create a directory.
196
     *
197
     * @param string $dirname directory name
198
     * @param Config $config
199
     *
200
     * @return bool|array
201
     */
202 2
    public function createDir($dirname, Config $config)
203
    {
204 2
        return $this->upload($dirname . '/', '', $config);
205
    }
206
207
    /**
208
     * Check whether a file exists.
209
     *
210
     * @param string $path
211
     *
212
     * @return bool
213
     */
214 12
    public function has($path)
215
    {
216 12
        $location = $this->applyPathPrefix($path);
217
218 12
        if ($this->s3Client->doesObjectExist($this->bucket, $location)) {
219 2
            return true;
220
        }
221
222 10
        return $this->doesDirectoryExist($location);
223
    }
224
225
    /**
226
     * Read a file.
227
     *
228
     * @param string $path
229
     *
230
     * @return false|array
231
     */
232 4
    public function read($path)
233
    {
234 4
        $response = $this->readObject($path);
235
236 4
        if ($response !== false) {
237 2
            $response['contents'] = $response['contents']->getContents();
238 2
        }
239
240 4
        return $response;
241
    }
242
243
    /**
244
     * List contents of a directory.
245
     *
246
     * @param string $directory
247
     * @param bool   $recursive
248
     *
249
     * @return array
250
     */
251 2
    public function listContents($directory = '', $recursive = false)
252
    {
253 2
        $prefix = $this->applyPathPrefix(rtrim($directory, '/') . '/');
254 2
        $options = ['Bucket' => $this->bucket, 'Prefix' => ltrim($prefix, '/')];
255
256 2
        if ($recursive === false) {
257 2
            $options['Delimiter'] = '/';
258 2
        }
259
260 2
        $listing = $this->retrievePaginatedListing($options);
261 2
        $normalizer = [$this, 'normalizeResponse'];
262 2
        $normalized = array_map($normalizer, $listing);
263
264 2
        return Util::emulateDirectories($normalized);
265
    }
266
267
    /**
268
     * @param array $options
269
     *
270
     * @return array
271
     */
272 2
    protected function retrievePaginatedListing(array $options)
273
    {
274 2
        $resultPaginator = $this->s3Client->getPaginator('ListObjects', $options);
275 2
        $listing = [];
276
277 2
        foreach ($resultPaginator as $result) {
278
            $listing = array_merge($listing, $result->get('Contents') ?: [], $result->get('CommonPrefixes') ?: []);
279 2
        }
280
281 2
        return $listing;
282
    }
283
284
    /**
285
     * Get all the meta data of a file or directory.
286
     *
287
     * @param string $path
288
     *
289
     * @return false|array
290
     */
291 12
    public function getMetadata($path)
292
    {
293 12
        $command = $this->s3Client->getCommand(
294 12
            'headObject',
295
            [
296 12
                'Bucket' => $this->bucket,
297 12
                'Key' => $this->applyPathPrefix($path),
298
            ]
299 12
        );
300
301
        /* @var Result $result */
302
        try {
303 12
            $result = $this->s3Client->execute($command);
304 12
        } catch (S3Exception $exception) {
305 4
            $response = $exception->getResponse();
306
307 4
            if ($response !== null && $response->getStatusCode() === 404) {
308 2
                return false;
309
            }
310
311 2
            throw $exception;
312
        }
313
314 8
        return $this->normalizeResponse($result->toArray(), $path);
315
    }
316
317
    /**
318
     * Get all the meta data of a file or directory.
319
     *
320
     * @param string $path
321
     *
322
     * @return false|array
323
     */
324 2
    public function getSize($path)
325
    {
326 2
        return $this->getMetadata($path);
327
    }
328
329
    /**
330
     * Get the mimetype of a file.
331
     *
332
     * @param string $path
333
     *
334
     * @return false|array
335
     */
336 2
    public function getMimetype($path)
337
    {
338 2
        return $this->getMetadata($path);
339
    }
340
341
    /**
342
     * Get the timestamp of a file.
343
     *
344
     * @param string $path
345
     *
346
     * @return false|array
347
     */
348 2
    public function getTimestamp($path)
349
    {
350 2
        return $this->getMetadata($path);
351
    }
352
353
    /**
354
     * Write a new file using a stream.
355
     *
356
     * @param string   $path
357
     * @param resource $resource
358
     * @param Config   $config Config object
359
     *
360
     * @return array|false false on failure file meta data on success
361
     */
362 2
    public function writeStream($path, $resource, Config $config)
363
    {
364 2
        return $this->upload($path, $resource, $config);
365
    }
366
367
    /**
368
     * Update a file using a stream.
369
     *
370
     * @param string   $path
371
     * @param resource $resource
372
     * @param Config   $config Config object
373
     *
374
     * @return array|false false on failure file meta data on success
375
     */
376 2
    public function updateStream($path, $resource, Config $config)
377
    {
378 2
        return $this->upload($path, $resource, $config);
379
    }
380
381
    /**
382
     * Copy a file.
383
     *
384
     * @param string $path
385
     * @param string $newpath
386
     *
387
     * @return bool
388
     */
389 8
    public function copy($path, $newpath)
390
    {
391 8
        $command = $this->s3Client->getCommand(
392 8
            'copyObject',
393
            [
394 8
                'Bucket' => $this->bucket,
395 8
                'Key' => $this->applyPathPrefix($newpath),
396 8
                'CopySource' => urlencode($this->bucket . '/' . $this->applyPathPrefix($path)),
397 8
                'ACL' => $this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC
398 8
                    ? 'public-read' : 'private',
399 8
            ] + $this->options
400 8
        );
401
402
        try {
403 8
            $this->s3Client->execute($command);
404 8
        } catch (S3Exception $e) {
405 4
            return false;
406
        }
407
408 4
        return true;
409
    }
410
411
    /**
412
     * Read a file as a stream.
413
     *
414
     * @param string $path
415
     *
416
     * @return array|false
417
     */
418 4
    public function readStream($path)
419
    {
420 4
        $response = $this->readObject($path);
421
422 4
        if ($response !== false) {
423 4
            $response['stream'] = $response['contents']->detach();
424 4
            unset($response['contents']);
425 4
        }
426
427 4
        return $response;
428
    }
429
430
    /**
431
     * Read an object and normalize the response.
432
     *
433
     * @param $path
434
     *
435
     * @return array|bool
436
     */
437 8
    protected function readObject($path)
438
    {
439
        $options = [
440 8
            'Bucket' => $this->bucket,
441 8
            'Key' => $this->applyPathPrefix($path),
442 8
        ];
443
444 8
        if (isset($this->options['@http'])) {
445 2
            $options['@http'] = $this->options['@http'];
446 2
        }
447
448 8
        $command = $this->s3Client->getCommand('getObject', $options);
449
450
        try {
451
            /** @var Result $response */
452 8
            $response = $this->s3Client->execute($command);
453 8
        } catch (S3Exception $e) {
454 2
            return false;
455
        }
456
457 6
        return $this->normalizeResponse($response->toArray(), $path);
458
    }
459
460
    /**
461
     * Set the visibility for a file.
462
     *
463
     * @param string $path
464
     * @param string $visibility
465
     *
466
     * @return array|false file meta data
467
     */
468 6
    public function setVisibility($path, $visibility)
469
    {
470 6
        $command = $this->s3Client->getCommand(
471 6
            'putObjectAcl',
472
            [
473 6
                'Bucket' => $this->bucket,
474 6
                'Key' => $this->applyPathPrefix($path),
475 6
                'ACL' => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
476
            ]
477 6
        );
478
479
        try {
480 6
            $this->s3Client->execute($command);
481 6
        } catch (S3Exception $exception) {
482 2
            return false;
483
        }
484
485 4
        return compact('path', 'visibility');
486
    }
487
488
    /**
489
     * Get the visibility of a file.
490
     *
491
     * @param string $path
492
     *
493
     * @return array|false
494
     */
495 4
    public function getVisibility($path)
496
    {
497 4
        return ['visibility' => $this->getRawVisibility($path)];
498
    }
499
500
    /**
501
     * {@inheritdoc}
502
     */
503 64
    public function applyPathPrefix($prefix)
504
    {
505 64
        return ltrim(parent::applyPathPrefix($prefix), '/');
506
    }
507
508
    /**
509
     * {@inheritdoc}
510
     */
511 72
    public function setPathPrefix($prefix)
512
    {
513 72
        $prefix = ltrim($prefix, '/');
514
515 72
        return parent::setPathPrefix($prefix);
516
    }
517
518
    /**
519
     * Get the object acl presented as a visibility.
520
     *
521
     * @param string $path
522
     *
523
     * @return string
524
     */
525 12
    protected function getRawVisibility($path)
526
    {
527 12
        $command = $this->s3Client->getCommand(
528 12
            'getObjectAcl',
529
            [
530 12
                'Bucket' => $this->bucket,
531 12
                'Key' => $this->applyPathPrefix($path),
532
            ]
533 12
        );
534
535 12
        $result = $this->s3Client->execute($command);
536 12
        $visibility = AdapterInterface::VISIBILITY_PRIVATE;
537
538 12
        foreach ($result->get('Grants') as $grant) {
539
            if (
540 2
                isset($grant['Grantee']['URI'])
541 2
                && $grant['Grantee']['URI'] === self::PUBLIC_GRANT_URI
542 2
                && $grant['Permission'] === 'READ'
543 2
            ) {
544 2
                $visibility = AdapterInterface::VISIBILITY_PUBLIC;
545 2
                break;
546
            }
547 12
        }
548
549 12
        return $visibility;
550
    }
551
552
    /**
553
     * Upload an object.
554
     *
555
     * @param        $path
556
     * @param        $body
557
     * @param Config $config
558
     *
559
     * @return array
560
     */
561 10
    protected function upload($path, $body, Config $config)
562
    {
563 10
        $key = $this->applyPathPrefix($path);
564 10
        $options = $this->getOptionsFromConfig($config);
565 10
        $acl = array_key_exists('ACL', $options) ? $options['ACL'] : 'private';
566
567 10
        if ( ! isset($options['ContentType'])) {
568 2
            $options['ContentType'] = Util::guessMimeType($path, $body);
569 2
        }
570
571 10
        if ( ! isset($options['ContentLength'])) {
572 10
            $options['ContentLength'] = is_string($body) ? Util::contentSize($body) : Util::getStreamSize($body);
573 10
        }
574
575 10
        if ($options['ContentLength'] === null) {
576
            unset($options['ContentLength']);
577
        }
578
579 10
        $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
580
581 10
        return $this->normalizeResponse($options, $key);
582
    }
583
584
    /**
585
     * Get options from the config.
586
     *
587
     * @param Config $config
588
     *
589
     * @return array
590
     */
591 10
    protected function getOptionsFromConfig(Config $config)
592
    {
593 10
        $options = $this->options;
594
595 10
        if ($visibility = $config->get('visibility')) {
596
            // For local reference
597 8
            $options['visibility'] = $visibility;
598
            // For external reference
599 8
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
600 8
        }
601
602 10
        if ($mimetype = $config->get('mimetype')) {
603
            // For local reference
604 8
            $options['mimetype'] = $mimetype;
605
            // For external reference
606 8
            $options['ContentType'] = $mimetype;
607 8
        }
608
609 10
        foreach (static::$metaOptions as $option) {
610 10
            if ( ! $config->has($option)) {
611 10
                continue;
612
            }
613 8
            $options[$option] = $config->get($option);
614 10
        }
615
616 10
        return $options;
617
    }
618
619
    /**
620
     * Normalize the object result array.
621
     *
622
     * @param array  $response
623
     * @param string $path
624
     *
625
     * @return array
626
     */
627 24
    protected function normalizeResponse(array $response, $path = null)
628
    {
629
        $result = [
630 24
            'path' => $path ?: $this->removePathPrefix(
631
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
632 24
            ),
633 24
        ];
634 24
        $result = array_merge($result, Util::pathinfo($result['path']));
635
636 24
        if (isset($response['LastModified'])) {
637 14
            $result['timestamp'] = strtotime($response['LastModified']);
638 14
        }
639
640 24
        if (substr($result['path'], -1) === '/') {
641 2
            $result['type'] = 'dir';
642 2
            $result['path'] = rtrim($result['path'], '/');
643
644 2
            return $result;
645
        }
646
647 22
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
648
    }
649
650
    /**
651
     * @param $location
652
     *
653
     * @return bool
654
     */
655 10
    protected function doesDirectoryExist($location)
656
    {
657
        // Maybe this isn't an actual key, but a prefix.
658
        // Do a prefix listing of objects to determine.
659 10
        $command = $this->s3Client->getCommand(
660 10
            'listObjects',
661
            [
662 10
                'Bucket' => $this->bucket,
663 10
                'Prefix' => rtrim($location, '/') . '/',
664 10
                'MaxKeys' => 1,
665
            ]
666 10
        );
667
668
        try {
669 10
            $result = $this->s3Client->execute($command);
670
671 6
            return $result['Contents'] || $result['CommonPrefixes'];
672 4
        } catch (S3Exception $e) {
673 4
            if ($e->getStatusCode() === 403) {
674 2
                return false;
675
            }
676
677 2
            throw $e;
678
        }
679
    }
680
}
681