Completed
Push — master ( 264ef5...af7384 )
by Frank
02:49
created

AwsS3Adapter::getSize()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
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 2
        );
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 3
        } 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 2
        }
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 2
        }
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 2
        }
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 7
        );
315
316
        /* @var Result $result */
317
        try {
318 14
            $result = $this->s3Client->execute($command);
319 9
        } 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 8
        $command = $this->s3Client->getCommand(
419 8
            'copyObject',
420
            [
421 8
                'Bucket'     => $this->bucket,
422 8
                'Key'        => $this->applyPathPrefix($newpath),
423 8
                'CopySource' => S3Client::encodeKey($this->bucket . '/' . $this->applyPathPrefix($path)),
424 8
                'ACL'        => $this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC
425 8
                    ? 'public-read' : 'private',
426 8
            ] + $this->options
427 4
        );
428
429
        try {
430 8
            $this->s3Client->execute($command);
431 6
        } catch (S3Exception $e) {
432 4
            return false;
433
        }
434
435 4
        return true;
436
    }
437
438
    /**
439
     * Read a file as a stream.
440
     *
441
     * @param string $path
442
     *
443
     * @return array|false
444
     */
445 10
    public function readStream($path)
446
    {
447 10
        $response = $this->readObject($path);
448
449 10
        if ($response !== false) {
450 10
            $response['stream'] = $response['contents']->detach();
451 10
            unset($response['contents']);
452 5
        }
453
454 10
        return $response;
455
    }
456
457
    /**
458
     * Read an object and normalize the response.
459
     *
460
     * @param string $path
461
     *
462
     * @return array|bool
463
     */
464 14
    protected function readObject($path)
465
    {
466
        $options = [
467 14
            'Bucket' => $this->bucket,
468 14
            'Key'    => $this->applyPathPrefix($path),
469 14
        ] + $this->options;
470
471 14
        if ($this->streamReads && ! isset($options['@http']['stream'])) {
472 8
            $options['@http']['stream'] = true;
473 4
        }
474
475 14
        $command = $this->s3Client->getCommand('getObject', $options + $this->options);
476
477
        try {
478
            /** @var Result $response */
479 14
            $response = $this->s3Client->execute($command);
480 8
        } catch (S3Exception $e) {
481 2
            return false;
482
        }
483
484 12
        return $this->normalizeResponse($response->toArray(), $path);
485
    }
486
487
    /**
488
     * Set the visibility for a file.
489
     *
490
     * @param string $path
491
     * @param string $visibility
492
     *
493
     * @return array|false file meta data
494
     */
495 6
    public function setVisibility($path, $visibility)
496
    {
497 6
        $command = $this->s3Client->getCommand(
498 6
            'putObjectAcl',
499
            [
500 6
                'Bucket' => $this->bucket,
501 6
                'Key'    => $this->applyPathPrefix($path),
502 6
                'ACL'    => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
503
            ]
504 3
        );
505
506
        try {
507 6
            $this->s3Client->execute($command);
508 4
        } catch (S3Exception $exception) {
509 2
            return false;
510
        }
511
512 4
        return compact('path', 'visibility');
513
    }
514
515
    /**
516
     * Get the visibility of a file.
517
     *
518
     * @param string $path
519
     *
520
     * @return array|false
521
     */
522 4
    public function getVisibility($path)
523
    {
524 4
        return ['visibility' => $this->getRawVisibility($path)];
525
    }
526
527
    /**
528
     * {@inheritdoc}
529
     */
530 72
    public function applyPathPrefix($path)
531
    {
532 72
        return ltrim(parent::applyPathPrefix($path), '/');
533
    }
534
535
    /**
536
     * {@inheritdoc}
537
     */
538 80
    public function setPathPrefix($prefix)
539
    {
540 80
        $prefix = ltrim($prefix, '/');
541
542 80
        return parent::setPathPrefix($prefix);
543
    }
544
545
    /**
546
     * Get the object acl presented as a visibility.
547
     *
548
     * @param string $path
549
     *
550
     * @return string
551
     */
552 12
    protected function getRawVisibility($path)
553
    {
554 12
        $command = $this->s3Client->getCommand(
555 12
            'getObjectAcl',
556
            [
557 12
                'Bucket' => $this->bucket,
558 12
                'Key'    => $this->applyPathPrefix($path),
559
            ]
560 6
        );
561
562 12
        $result = $this->s3Client->execute($command);
563 12
        $visibility = AdapterInterface::VISIBILITY_PRIVATE;
564
565 12
        foreach ($result->get('Grants') as $grant) {
566
            if (
567 2
                isset($grant['Grantee']['URI'])
568 2
                && $grant['Grantee']['URI'] === self::PUBLIC_GRANT_URI
569 2
                && $grant['Permission'] === 'READ'
570 1
            ) {
571 2
                $visibility = AdapterInterface::VISIBILITY_PUBLIC;
572 2
                break;
573
            }
574 6
        }
575
576 12
        return $visibility;
577
    }
578
579
    /**
580
     * Upload an object.
581
     *
582
     * @param string          $path
583
     * @param string|resource $body
584
     * @param Config          $config
585
     *
586
     * @return array|bool
587
     */
588 14
    protected function upload($path, $body, Config $config)
589
    {
590 14
        $key = $this->applyPathPrefix($path);
591 14
        $options = $this->getOptionsFromConfig($config);
592 14
        $acl = array_key_exists('ACL', $options) ? $options['ACL'] : 'private';
593
594 14
        if (!$this->isOnlyDir($path)) {
595 12
            if ( ! isset($options['ContentType'])) {
596 2
                $options['ContentType'] = Util::guessMimeType($path, $body);
597 1
            }
598
599 12
            if ( ! isset($options['ContentLength'])) {
600 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 588 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...
601 6
            }
602
603 12
            if ($options['ContentLength'] === null) {
604
                unset($options['ContentLength']);
605
            }
606 6
        }
607
608
        try {
609 14
            $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
610 8
        } catch (S3MultipartUploadException $multipartUploadException) {
611 2
            return false;
612
        }
613
614 12
        return $this->normalizeResponse($options, $path);
615
    }
616
617
    /**
618
     * Check if the path contains only directories
619
     *
620
     * @param string $path
621
     *
622
     * @return bool
623
     */
624 34
    private function isOnlyDir($path)
625
    {
626 34
        return substr($path, -1) === '/';
627
    }
628
629
    /**
630
     * Get options from the config.
631
     *
632
     * @param Config $config
633
     *
634
     * @return array
635
     */
636 14
    protected function getOptionsFromConfig(Config $config)
637
    {
638 14
        $options = $this->options;
639
640 14
        if ($visibility = $config->get('visibility')) {
641
            // For local reference
642 10
            $options['visibility'] = $visibility;
643
            // For external reference
644 10
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
645 5
        }
646
647 14
        if ($mimetype = $config->get('mimetype')) {
648
            // For local reference
649 10
            $options['mimetype'] = $mimetype;
650
            // For external reference
651 10
            $options['ContentType'] = $mimetype;
652 5
        }
653
654 14
        foreach (static::$metaOptions as $option) {
655 14
            if ( ! $config->has($option)) {
656 14
                continue;
657
            }
658 10
            $options[$option] = $config->get($option);
659 7
        }
660
661 14
        return $options;
662
    }
663
664
    /**
665
     * Normalize the object result array.
666
     *
667
     * @param array  $response
668
     * @param string $path
669
     *
670
     * @return array
671
     */
672 32
    protected function normalizeResponse(array $response, $path = null)
673
    {
674
        $result = [
675 32
            'path' => $path ?: $this->removePathPrefix(
676 18
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
677 16
            ),
678 16
        ];
679 32
        $result = array_merge($result, Util::pathinfo($result['path']));
680
681 32
        if (isset($response['LastModified'])) {
682 20
            $result['timestamp'] = strtotime($response['LastModified']);
683 10
        }
684
685 32
        if ($this->isOnlyDir($result['path'])) {
686 6
            $result['type'] = 'dir';
687 6
            $result['path'] = rtrim($result['path'], '/');
688
689 6
            return $result;
690
        }
691
692 30
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
693
    }
694
695
    /**
696
     * @param string $location
697
     *
698
     * @return bool
699
     */
700 10
    protected function doesDirectoryExist($location)
701
    {
702
        // Maybe this isn't an actual key, but a prefix.
703
        // Do a prefix listing of objects to determine.
704 10
        $command = $this->s3Client->getCommand(
705 10
            'listObjects',
706
            [
707 10
                'Bucket'  => $this->bucket,
708 10
                'Prefix'  => rtrim($location, '/') . '/',
709 10
                'MaxKeys' => 1,
710
            ]
711 5
        );
712
713
        try {
714 10
            $result = $this->s3Client->execute($command);
715
716 6
            return $result['Contents'] || $result['CommonPrefixes'];
717 4
        } catch (S3Exception $e) {
718 4
            if (in_array($e->getStatusCode(), [403, 404], true)) {
719 2
                return false;
720
            }
721
722 2
            throw $e;
723
        }
724
    }
725
}
726