Completed
Push — master ( a67a9c...398c56 )
by Frank
02:30
created

AwsS3Adapter::retrievePaginatedListing()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 9.9
c 0
b 0
f 0
cc 4
nc 2
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\S3Client;
9
use League\Flysystem\Adapter\AbstractAdapter;
10
use League\Flysystem\Adapter\CanOverwriteFiles;
11
use League\Flysystem\AdapterInterface;
12
use League\Flysystem\Config;
13
use League\Flysystem\Util;
14
15
class AwsS3Adapter extends AbstractAdapter implements CanOverwriteFiles
16
{
17
    const PUBLIC_GRANT_URI = 'http://acs.amazonaws.com/groups/global/AllUsers';
18
19
    /**
20
     * @var array
21
     */
22
    protected static $resultMap = [
23
        'Body'          => 'contents',
24
        'ContentLength' => 'size',
25
        'ContentType'   => 'mimetype',
26
        'Size'          => 'size',
27
        'Metadata'      => 'metadata',
28
        'StorageClass'  => 'storageclass',
29
        'ETag'          => 'etag',
30
        'VersionId'     => 'versionid'
31
    ];
32
33
    /**
34
     * @var array
35
     */
36
    protected static $metaOptions = [
37
        'ACL',
38
        'CacheControl',
39
        'ContentDisposition',
40
        'ContentEncoding',
41
        'ContentLength',
42
        'ContentType',
43
        'Expires',
44
        'GrantFullControl',
45
        'GrantRead',
46
        'GrantReadACP',
47
        'GrantWriteACP',
48
        'Metadata',
49
        'RequestPayer',
50
        'SSECustomerAlgorithm',
51
        'SSECustomerKey',
52
        'SSECustomerKeyMD5',
53
        'SSEKMSKeyId',
54
        'ServerSideEncryption',
55
        'StorageClass',
56
        'Tagging',
57
        'WebsiteRedirectLocation',
58
    ];
59
60
    /**
61
     * @var S3Client
62
     */
63
    protected $s3Client;
64
65
    /**
66
     * @var string
67
     */
68
    protected $bucket;
69
70
    /**
71
     * @var array
72
     */
73
    protected $options = [];
74
75
    /**
76
     * Constructor.
77
     *
78
     * @param S3Client $client
79
     * @param string   $bucket
80
     * @param string   $prefix
81
     * @param array    $options
82
     */
83 74
    public function __construct(S3Client $client, $bucket, $prefix = '', array $options = [])
84
    {
85 74
        $this->s3Client = $client;
86 74
        $this->bucket = $bucket;
87 74
        $this->setPathPrefix($prefix);
88 74
        $this->options = $options;
89 74
    }
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 S3Client
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 4
    public function write($path, $contents, Config $config)
131
    {
132 4
        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
            $response = $exception->getResponse();
321
322 4
            if ($response !== null && $response->getStatusCode() === 404) {
323 2
                return false;
324
            }
325
326 2
            throw $exception;
327
        }
328
329 10
        return $this->normalizeResponse($result->toArray(), $path);
330
    }
331
332
    /**
333
     * Get all the meta data of a file or directory.
334
     *
335
     * @param string $path
336
     *
337
     * @return false|array
338
     */
339 2
    public function getSize($path)
340
    {
341 2
        return $this->getMetadata($path);
342
    }
343
344
    /**
345
     * Get the mimetype of a file.
346
     *
347
     * @param string $path
348
     *
349
     * @return false|array
350
     */
351 2
    public function getMimetype($path)
352
    {
353 2
        return $this->getMetadata($path);
354
    }
355
356
    /**
357
     * Get the timestamp of a file.
358
     *
359
     * @param string $path
360
     *
361
     * @return false|array
362
     */
363 2
    public function getTimestamp($path)
364
    {
365 2
        return $this->getMetadata($path);
366
    }
367
368
    /**
369
     * Write a new file using a stream.
370
     *
371
     * @param string   $path
372
     * @param resource $resource
373
     * @param Config   $config Config object
374
     *
375
     * @return array|false false on failure file meta data on success
376
     */
377 4
    public function writeStream($path, $resource, Config $config)
378
    {
379 4
        return $this->upload($path, $resource, $config);
380
    }
381
382
    /**
383
     * Update a file using a stream.
384
     *
385
     * @param string   $path
386
     * @param resource $resource
387
     * @param Config   $config Config object
388
     *
389
     * @return array|false false on failure file meta data on success
390
     */
391 4
    public function updateStream($path, $resource, Config $config)
392
    {
393 4
        return $this->upload($path, $resource, $config);
394
    }
395
396
    /**
397
     * Copy a file.
398
     *
399
     * @param string $path
400
     * @param string $newpath
401
     *
402
     * @return bool
403
     */
404 8
    public function copy($path, $newpath)
405
    {
406 8
        $command = $this->s3Client->getCommand(
407 8
            'copyObject',
408
            [
409 8
                'Bucket'     => $this->bucket,
410 8
                'Key'        => $this->applyPathPrefix($newpath),
411 8
                'CopySource' => urlencode($this->bucket . '/' . $this->applyPathPrefix($path)),
412 8
                'ACL'        => $this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC
413 8
                    ? 'public-read' : 'private',
414 8
            ] + $this->options
415
        );
416
417
        try {
418 8
            $this->s3Client->execute($command);
419 4
        } catch (S3Exception $e) {
420 4
            return false;
421
        }
422
423 4
        return true;
424
    }
425
426
    /**
427
     * Read a file as a stream.
428
     *
429
     * @param string $path
430
     *
431
     * @return array|false
432
     */
433 6
    public function readStream($path)
434
    {
435 6
        $response = $this->readObject($path);
436
437 6
        if ($response !== false) {
438 6
            $response['stream'] = $response['contents']->detach();
439 6
            unset($response['contents']);
440
        }
441
442 6
        return $response;
443
    }
444
445
    /**
446
     * Read an object and normalize the response.
447
     *
448
     * @param $path
449
     *
450
     * @return array|bool
451
     */
452 10
    protected function readObject($path)
453
    {
454
        $options = [
455 10
            'Bucket' => $this->bucket,
456 10
            'Key'    => $this->applyPathPrefix($path),
457
        ];
458
459 10
        if (isset($this->options['@http'])) {
460 2
            $options['@http'] = $this->options['@http'];
461
        }
462
463 10
        $command = $this->s3Client->getCommand('getObject', $options + $this->options);
464
465
        try {
466
            /** @var Result $response */
467 10
            $response = $this->s3Client->execute($command);
468 2
        } catch (S3Exception $e) {
469 2
            return false;
470
        }
471
472 8
        return $this->normalizeResponse($response->toArray(), $path);
473
    }
474
475
    /**
476
     * Set the visibility for a file.
477
     *
478
     * @param string $path
479
     * @param string $visibility
480
     *
481
     * @return array|false file meta data
482
     */
483 6
    public function setVisibility($path, $visibility)
484
    {
485 6
        $command = $this->s3Client->getCommand(
486 6
            'putObjectAcl',
487
            [
488 6
                'Bucket' => $this->bucket,
489 6
                'Key'    => $this->applyPathPrefix($path),
490 6
                'ACL'    => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
491
            ]
492
        );
493
494
        try {
495 6
            $this->s3Client->execute($command);
496 2
        } catch (S3Exception $exception) {
497 2
            return false;
498
        }
499
500 4
        return compact('path', 'visibility');
501
    }
502
503
    /**
504
     * Get the visibility of a file.
505
     *
506
     * @param string $path
507
     *
508
     * @return array|false
509
     */
510 4
    public function getVisibility($path)
511
    {
512 4
        return ['visibility' => $this->getRawVisibility($path)];
513
    }
514
515
    /**
516
     * {@inheritdoc}
517
     */
518 66
    public function applyPathPrefix($path)
519
    {
520 66
        return ltrim(parent::applyPathPrefix($path), '/');
521
    }
522
523
    /**
524
     * {@inheritdoc}
525
     */
526 74
    public function setPathPrefix($prefix)
527
    {
528 74
        $prefix = ltrim($prefix, '/');
529
530 74
        return parent::setPathPrefix($prefix);
531
    }
532
533
    /**
534
     * Get the object acl presented as a visibility.
535
     *
536
     * @param string $path
537
     *
538
     * @return string
539
     */
540 12
    protected function getRawVisibility($path)
541
    {
542 12
        $command = $this->s3Client->getCommand(
543 12
            'getObjectAcl',
544
            [
545 12
                'Bucket' => $this->bucket,
546 12
                'Key'    => $this->applyPathPrefix($path),
547
            ]
548
        );
549
550 12
        $result = $this->s3Client->execute($command);
551 12
        $visibility = AdapterInterface::VISIBILITY_PRIVATE;
552
553 12
        foreach ($result->get('Grants') as $grant) {
554
            if (
555 2
                isset($grant['Grantee']['URI'])
556 2
                && $grant['Grantee']['URI'] === self::PUBLIC_GRANT_URI
557 2
                && $grant['Permission'] === 'READ'
558
            ) {
559 2
                $visibility = AdapterInterface::VISIBILITY_PUBLIC;
560 2
                break;
561
            }
562
        }
563
564 12
        return $visibility;
565
    }
566
567
    /**
568
     * Upload an object.
569
     *
570
     * @param        $path
571
     * @param        $body
572
     * @param Config $config
573
     *
574
     * @return array
575
     */
576 12
    protected function upload($path, $body, Config $config)
577
    {
578 12
        $key = $this->applyPathPrefix($path);
579 12
        $options = $this->getOptionsFromConfig($config);
580 12
        $acl = array_key_exists('ACL', $options) ? $options['ACL'] : 'private';
581
582 12
        if ( ! isset($options['ContentType'])) {
583 4
            $options['ContentType'] = Util::guessMimeType($path, $body);
584
        }
585
586 12
        if ( ! isset($options['ContentLength'])) {
587 12
            $options['ContentLength'] = is_string($body) ? Util::contentSize($body) : Util::getStreamSize($body);
588
        }
589
590 12
        if ($options['ContentLength'] === null) {
591
            unset($options['ContentLength']);
592
        }
593
594 12
        $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
595
596 12
        return $this->normalizeResponse($options, $path);
597
    }
598
599
    /**
600
     * Get options from the config.
601
     *
602
     * @param Config $config
603
     *
604
     * @return array
605
     */
606 12
    protected function getOptionsFromConfig(Config $config)
607
    {
608 12
        $options = $this->options;
609
610 12
        if ($visibility = $config->get('visibility')) {
611
            // For local reference
612 8
            $options['visibility'] = $visibility;
613
            // For external reference
614 8
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
615
        }
616
617 12
        if ($mimetype = $config->get('mimetype')) {
618
            // For local reference
619 8
            $options['mimetype'] = $mimetype;
620
            // For external reference
621 8
            $options['ContentType'] = $mimetype;
622
        }
623
624 12
        foreach (static::$metaOptions as $option) {
625 12
            if ( ! $config->has($option)) {
626 12
                continue;
627
            }
628 8
            $options[$option] = $config->get($option);
629
        }
630
631 12
        return $options;
632
    }
633
634
    /**
635
     * Normalize the object result array.
636
     *
637
     * @param array  $response
638
     * @param string $path
639
     *
640
     * @return array
641
     */
642 28
    protected function normalizeResponse(array $response, $path = null)
643
    {
644
        $result = [
645 28
            'path' => $path ?: $this->removePathPrefix(
646 28
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
647
            ),
648
        ];
649 28
        $result = array_merge($result, Util::pathinfo($result['path']));
650
651 28
        if (isset($response['LastModified'])) {
652 16
            $result['timestamp'] = strtotime($response['LastModified']);
653
        }
654
655 28
        if (substr($result['path'], -1) === '/') {
656 6
            $result['type'] = 'dir';
657 6
            $result['path'] = rtrim($result['path'], '/');
658
659 6
            return $result;
660
        }
661
662 26
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
663
    }
664
665
    /**
666
     * @param $location
667
     *
668
     * @return bool
669
     */
670 10
    protected function doesDirectoryExist($location)
671
    {
672
        // Maybe this isn't an actual key, but a prefix.
673
        // Do a prefix listing of objects to determine.
674 10
        $command = $this->s3Client->getCommand(
675 10
            'listObjects',
676
            [
677 10
                'Bucket'  => $this->bucket,
678 10
                'Prefix'  => rtrim($location, '/') . '/',
679 10
                'MaxKeys' => 1,
680
            ]
681
        );
682
683
        try {
684 10
            $result = $this->s3Client->execute($command);
685
686 6
            return $result['Contents'] || $result['CommonPrefixes'];
687 4
        } catch (S3Exception $e) {
688 4
            if ($e->getStatusCode() === 403) {
689 2
                return false;
690
            }
691
692 2
            throw $e;
693
        }
694
    }
695
}
696