Completed
Pull Request — master (#189)
by
unknown
01:57
created

AwsS3Adapter::doesDirectoryExist()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 12
cts 12
cp 1
rs 9.52
c 0
b 0
f 0
cc 4
nc 8
nop 1
crap 4
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' => 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
        );
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 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
        ];
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 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
        /* Ensure directory components exist */
596 14
        $directory = Util::dirname($path);
597 14
        if ($directory !== '') {
598 2
            $this->createDir($directory, $config);
599
        }
600
601 14
        if (!$this->isOnlyDir($path)) {
602 12
            if ( ! isset($options['ContentType'])) {
603 2
                $options['ContentType'] = Util::guessMimeType($path, $body);
604
            }
605
606 12
            if ( ! isset($options['ContentLength'])) {
607 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...
608
            }
609
610 12
            if ($options['ContentLength'] === null) {
611 12
                unset($options['ContentLength']);
612
            }
613
        } else {
614 4
            $options['ContentType'] = "application/x-directory";
615
        }
616
617
        try {
618 14
            $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
619 2
        } catch (S3MultipartUploadException $multipartUploadException) {
620 2
            return false;
621
        }
622
623 12
        return $this->normalizeResponse($options, $path);
624
    }
625
626
    /**
627
     * Check if the path contains only directories
628
     *
629
     * @param string $path
630
     *
631
     * @return bool
632
     */
633 30
    private function isOnlyDir($path)
634
    {
635 30
        return substr($path, -1) === '/';
636
    }
637
638
    /**
639
     * Get options from the config.
640
     *
641
     * @param Config $config
642
     *
643
     * @return array
644
     */
645 14
    protected function getOptionsFromConfig(Config $config)
646
    {
647 14
        $options = $this->options;
648
649 14
        if ($visibility = $config->get('visibility')) {
650
            // For local reference
651 10
            $options['visibility'] = $visibility;
652
            // For external reference
653 10
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
654
        }
655
656 14
        if ($mimetype = $config->get('mimetype')) {
657
            // For local reference
658 10
            $options['mimetype'] = $mimetype;
659
            // For external reference
660 10
            $options['ContentType'] = $mimetype;
661
        }
662
663 14
        foreach (static::$metaOptions as $option) {
664 14
            if ( ! $config->has($option)) {
665 14
                continue;
666
            }
667 10
            $options[$option] = $config->get($option);
668
        }
669
670 14
        return $options;
671
    }
672
673
    /**
674
     * Normalize the object result array.
675
     *
676
     * @param array  $response
677
     * @param string $path
678
     *
679
     * @return array
680
     */
681 28
    protected function normalizeResponse(array $response, $path = null)
682
    {
683
        $result = [
684 28
            'path' => $path ?: $this->removePathPrefix(
685 28
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
686
            ),
687
        ];
688 28
        $result = array_merge($result, Util::pathinfo($result['path']));
689
690 28
        if (isset($response['LastModified'])) {
691 16
            $result['timestamp'] = strtotime($response['LastModified']);
692
        }
693
694 28
        if ($this->isOnlyDir($result['path'])) {
695 6
            $result['type'] = 'dir';
696 6
            $result['path'] = rtrim($result['path'], '/');
697
698 6
            return $result;
699
        }
700
701 26
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
702
    }
703
704
    /**
705
     * @param string $location
706
     *
707
     * @return bool
708
     */
709 10
    protected function doesDirectoryExist($location)
710
    {
711
        // Maybe this isn't an actual key, but a prefix.
712
        // Do a prefix listing of objects to determine.
713 10
        $command = $this->s3Client->getCommand(
714 10
            'listObjects',
715
            [
716 10
                'Bucket'  => $this->bucket,
717 10
                'Prefix'  => rtrim($location, '/') . '/',
718 10
                'MaxKeys' => 1,
719
            ]
720
        );
721
722
        try {
723 10
            $result = $this->s3Client->execute($command);
724
725 6
            return $result['Contents'] || $result['CommonPrefixes'];
726 4
        } catch (S3Exception $e) {
727 4
            if ($e->getStatusCode() === 403) {
728 2
                return false;
729
            }
730
731 2
            throw $e;
732
        }
733
    }
734
}
735