Completed
Pull Request — master (#263)
by Stan
06:03
created

AwsS3Adapter   F

Complexity

Total Complexity 77

Size/Duplication

Total Lines 706
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 99.51%

Importance

Changes 0
Metric Value
wmc 77
lcom 1
cbo 8
dl 0
loc 706
ccs 202
cts 203
cp 0.9951
rs 2.134
c 0
b 0
f 0

34 Methods

Rating   Name   Duplication   Size   Complexity  
A getBucket() 0 4 1
A setBucket() 0 4 1
A getClient() 0 4 1
A write() 0 4 1
A update() 0 4 1
A rename() 0 8 2
A delete() 0 16 1
A deleteDir() 0 11 2
A createDir() 0 4 1
A has() 0 10 2
A read() 0 10 2
A listContents() 0 15 2
A retrievePaginatedListing() 0 11 4
A getMetadata() 0 23 3
A __construct() 0 8 1
A is404Exception() 0 10 3
A getSize() 0 4 1
A getMimetype() 0 4 1
A getTimestamp() 0 4 1
A writeStream() 0 4 1
A updateStream() 0 4 1
A getVisibility() 0 4 1
A applyPathPrefix() 0 4 1
A setPathPrefix() 0 6 1
A isOnlyDir() 0 4 1
A copy() 0 18 3
A readStream() 0 11 2
A readObject() 0 22 4
A setVisibility() 0 19 3
A getRawVisibility() 0 26 5
B upload() 0 28 8
B getOptionsFromConfig() 0 27 6
A normalizeResponse() 0 22 5
A doesDirectoryExist() 0 25 4

How to fix   Complexity   

Complex Class

Complex classes like AwsS3Adapter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AwsS3Adapter, and based on these observations, apply Extract Interface, too.

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