Completed
Push — master ( b3a6b9...891dbb )
by Frank
02:34
created

AwsS3Adapter::has()   B

Complexity

Conditions 3
Paths 5

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 3

Importance

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