Completed
Push — master ( e01b7a...438203 )
by Frank
02:08 queued 46s
created

AwsS3Adapter::readObject()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 3

Importance

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