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