Completed
Pull Request — master (#246)
by Youri
02:57
created

AwsS3Adapter::listContents()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 2

Importance

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

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
618 6
            }
619
620 12
            if ($options['ContentLength'] === null) {
621
                unset($options['ContentLength']);
622
            }
623 6
        }
624
625
        try {
626 14
            $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
627 8
        } catch (S3MultipartUploadException $multipartUploadException) {
628 2
            $this->logger->warning($multipartUploadException->getMessage());
629
630 2
            return false;
631
        }
632
633 12
        return $this->normalizeResponse($options, $path);
634
    }
635
636
    /**
637
     * Check if the path contains only directories
638
     *
639
     * @param string $path
640
     *
641
     * @return bool
642
     */
643 34
    private function isOnlyDir($path)
644
    {
645 34
        return substr($path, -1) === '/';
646
    }
647
648
    /**
649
     * Get options from the config.
650
     *
651
     * @param Config $config
652
     *
653
     * @return array
654
     */
655 14
    protected function getOptionsFromConfig(Config $config)
656
    {
657 14
        $options = $this->options;
658
659 14
        if ($visibility = $config->get('visibility')) {
660
            // For local reference
661 10
            $options['visibility'] = $visibility;
662
            // For external reference
663 10
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
664 5
        }
665
666 14
        if ($mimetype = $config->get('mimetype')) {
667
            // For local reference
668 10
            $options['mimetype'] = $mimetype;
669
            // For external reference
670 10
            $options['ContentType'] = $mimetype;
671 5
        }
672
673 14
        foreach (static::$metaOptions as $option) {
674 14
            if ( ! $config->has($option)) {
675 14
                continue;
676
            }
677 10
            $options[$option] = $config->get($option);
678 7
        }
679
680 14
        return $options;
681
    }
682
683
    /**
684
     * Normalize the object result array.
685
     *
686
     * @param array  $response
687
     * @param string $path
688
     *
689
     * @return array
690
     */
691 32
    protected function normalizeResponse(array $response, $path = null)
692
    {
693
        $result = [
694 32
            'path' => $path ?: $this->removePathPrefix(
695 18
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
696 16
            ),
697 16
        ];
698 32
        $result = array_merge($result, Util::pathinfo($result['path']));
699
700 32
        if (isset($response['LastModified'])) {
701 20
            $result['timestamp'] = strtotime($response['LastModified']);
702 10
        }
703
704 32
        if ($this->isOnlyDir($result['path'])) {
705 6
            $result['type'] = 'dir';
706 6
            $result['path'] = rtrim($result['path'], '/');
707
708 6
            return $result;
709
        }
710
711 30
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
712
    }
713
714
    /**
715
     * @param string $location
716
     *
717
     * @return bool
718
     */
719 10
    protected function doesDirectoryExist($location)
720
    {
721
        // Maybe this isn't an actual key, but a prefix.
722
        // Do a prefix listing of objects to determine.
723 10
        $command = $this->s3Client->getCommand(
724 10
            'listObjects',
725
            [
726 10
                'Bucket'  => $this->bucket,
727 10
                'Prefix'  => rtrim($location, '/') . '/',
728 10
                'MaxKeys' => 1,
729
            ]
730 5
        );
731
732
        try {
733 10
            $result = $this->s3Client->execute($command);
734
735 6
            return $result['Contents'] || $result['CommonPrefixes'];
736 4
        } catch (S3Exception $e) {
737 4
            if (in_array($e->getStatusCode(), [403, 404], true)) {
738 2
                return false;
739
            }
740
741 2
            throw $e;
742
        }
743
    }
744
}
745