Completed
Push — master ( 883b02...b35251 )
by Frank
03:05
created

AwsS3Adapter::is404Exception()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 9.9332
c 0
b 0
f 0
cc 3
nc 2
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\S3Client;
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 S3Client
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 S3Client $client
80
     * @param string   $bucket
81
     * @param string   $prefix
82
     * @param array    $options
83
     */
84 76
    public function __construct(S3Client $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 S3Client
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 4
    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
        );
185
186 4
        $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 2
        } 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
        }
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
        }
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
        }
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
        );
316
317
        /* @var Result $result */
318
        try {
319 14
            $result = $this->s3Client->execute($command);
320 4
        } 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' => urlencode($this->bucket . '/' . $this->applyPathPrefix($path)),
425 8
                'ACL'        => $this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC
426 8
                    ? 'public-read' : 'private',
427 8
            ] + $this->options
428
        );
429
430
        try {
431 8
            $this->s3Client->execute($command);
432 4
        } 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
        }
454
455 6
        return $response;
456
    }
457
458
    /**
459
     * Read an object and normalize the response.
460
     *
461
     * @param $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
        ];
471
472 10
        if (isset($this->options['@http'])) {
473 2
            $options['@http'] = $this->options['@http'];
474
        }
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 2
        } 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
        );
506
507
        try {
508 6
            $this->s3Client->execute($command);
509 2
        } 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
        );
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
            ) {
572 2
                $visibility = AdapterInterface::VISIBILITY_PUBLIC;
573 2
                break;
574
            }
575
        }
576
577 12
        return $visibility;
578
    }
579
580
    /**
581
     * Upload an object.
582
     *
583
     * @param        $path
584
     * @param        $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 ( ! isset($options['ContentType'])) {
596 4
            $options['ContentType'] = Util::guessMimeType($path, $body);
597
        }
598
599 14
        if ( ! isset($options['ContentLength'])) {
600 14
            $options['ContentLength'] = is_resource($body) ? Util::getStreamSize($body) : Util::contentSize($body);
601
        }
602
603 14
        if ($options['ContentLength'] === null) {
604
            unset($options['ContentLength']);
605
        }
606
607
        try {
608 14
            $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
609 2
        } catch (S3MultipartUploadException $multipartUploadException) {
610 2
            return false;
611
        }
612
613 12
        return $this->normalizeResponse($options, $path);
614
    }
615
616
    /**
617
     * Get options from the config.
618
     *
619
     * @param Config $config
620
     *
621
     * @return array
622
     */
623 14
    protected function getOptionsFromConfig(Config $config)
624
    {
625 14
        $options = $this->options;
626
627 14
        if ($visibility = $config->get('visibility')) {
628
            // For local reference
629 10
            $options['visibility'] = $visibility;
630
            // For external reference
631 10
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
632
        }
633
634 14
        if ($mimetype = $config->get('mimetype')) {
635
            // For local reference
636 10
            $options['mimetype'] = $mimetype;
637
            // For external reference
638 10
            $options['ContentType'] = $mimetype;
639
        }
640
641 14
        foreach (static::$metaOptions as $option) {
642 14
            if ( ! $config->has($option)) {
643 14
                continue;
644
            }
645 10
            $options[$option] = $config->get($option);
646
        }
647
648 14
        return $options;
649
    }
650
651
    /**
652
     * Normalize the object result array.
653
     *
654
     * @param array  $response
655
     * @param string $path
656
     *
657
     * @return array
658
     */
659 28
    protected function normalizeResponse(array $response, $path = null)
660
    {
661
        $result = [
662 28
            'path' => $path ?: $this->removePathPrefix(
663 28
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
664
            ),
665
        ];
666 28
        $result = array_merge($result, Util::pathinfo($result['path']));
667
668 28
        if (isset($response['LastModified'])) {
669 16
            $result['timestamp'] = strtotime($response['LastModified']);
670
        }
671
672 28
        if (substr($result['path'], -1) === '/') {
673 6
            $result['type'] = 'dir';
674 6
            $result['path'] = rtrim($result['path'], '/');
675
676 6
            return $result;
677
        }
678
679 26
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
680
    }
681
682
    /**
683
     * @param $location
684
     *
685
     * @return bool
686
     */
687 10
    protected function doesDirectoryExist($location)
688
    {
689
        // Maybe this isn't an actual key, but a prefix.
690
        // Do a prefix listing of objects to determine.
691 10
        $command = $this->s3Client->getCommand(
692 10
            'listObjects',
693
            [
694 10
                'Bucket'  => $this->bucket,
695 10
                'Prefix'  => rtrim($location, '/') . '/',
696 10
                'MaxKeys' => 1,
697
            ]
698
        );
699
700
        try {
701 10
            $result = $this->s3Client->execute($command);
702
703 6
            return $result['Contents'] || $result['CommonPrefixes'];
704 4
        } catch (S3Exception $e) {
705 4
            if ($e->getStatusCode() === 403) {
706 2
                return false;
707
            }
708
709 2
            throw $e;
710
        }
711
    }
712
}
713