Completed
Push — bugfix/catch-multipart-upload-... ( 12b7da...294113 )
by Frank
04:52 queued 01:58
created

AwsS3Adapter::has()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

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 2
nc 2
nop 1
crap 2
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 2
        );
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 3
        } 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 2
        }
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 2
        }
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 2
        }
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 7
        );
316
317
        /* @var Result $result */
318
        try {
319 14
            $result = $this->s3Client->execute($command);
320 9
        } catch (S3Exception $exception) {
321 4
            $response = $exception->getResponse();
322
323 4
            if ($response !== null && $response->getStatusCode() === 404) {
324 2
                return false;
325
            }
326
327 2
            throw $exception;
328
        }
329
330 10
        return $this->normalizeResponse($result->toArray(), $path);
331
    }
332
333
    /**
334
     * Get all the meta data of a file or directory.
335
     *
336
     * @param string $path
337
     *
338
     * @return false|array
339
     */
340 2
    public function getSize($path)
341
    {
342 2
        return $this->getMetadata($path);
343
    }
344
345
    /**
346
     * Get the mimetype of a file.
347
     *
348
     * @param string $path
349
     *
350
     * @return false|array
351
     */
352 2
    public function getMimetype($path)
353
    {
354 2
        return $this->getMetadata($path);
355
    }
356
357
    /**
358
     * Get the timestamp of a file.
359
     *
360
     * @param string $path
361
     *
362
     * @return false|array
363
     */
364 2
    public function getTimestamp($path)
365
    {
366 2
        return $this->getMetadata($path);
367
    }
368
369
    /**
370
     * Write a new file using a stream.
371
     *
372
     * @param string   $path
373
     * @param resource $resource
374
     * @param Config   $config Config object
375
     *
376
     * @return array|false false on failure file meta data on success
377
     */
378 4
    public function writeStream($path, $resource, Config $config)
379
    {
380 4
        return $this->upload($path, $resource, $config);
381
    }
382
383
    /**
384
     * Update a file using a stream.
385
     *
386
     * @param string   $path
387
     * @param resource $resource
388
     * @param Config   $config Config object
389
     *
390
     * @return array|false false on failure file meta data on success
391
     */
392 4
    public function updateStream($path, $resource, Config $config)
393
    {
394 4
        return $this->upload($path, $resource, $config);
395
    }
396
397
    /**
398
     * Copy a file.
399
     *
400
     * @param string $path
401
     * @param string $newpath
402
     *
403
     * @return bool
404
     */
405 8
    public function copy($path, $newpath)
406
    {
407 8
        $command = $this->s3Client->getCommand(
408 8
            'copyObject',
409
            [
410 8
                'Bucket'     => $this->bucket,
411 8
                'Key'        => $this->applyPathPrefix($newpath),
412 8
                'CopySource' => urlencode($this->bucket . '/' . $this->applyPathPrefix($path)),
413 8
                'ACL'        => $this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC
414 8
                    ? 'public-read' : 'private',
415 8
            ] + $this->options
416 4
        );
417
418
        try {
419 8
            $this->s3Client->execute($command);
420 6
        } catch (S3Exception $e) {
421 4
            return false;
422
        }
423
424 4
        return true;
425
    }
426
427
    /**
428
     * Read a file as a stream.
429
     *
430
     * @param string $path
431
     *
432
     * @return array|false
433
     */
434 6
    public function readStream($path)
435
    {
436 6
        $response = $this->readObject($path);
437
438 6
        if ($response !== false) {
439 6
            $response['stream'] = $response['contents']->detach();
440 6
            unset($response['contents']);
441 3
        }
442
443 6
        return $response;
444
    }
445
446
    /**
447
     * Read an object and normalize the response.
448
     *
449
     * @param $path
450
     *
451
     * @return array|bool
452
     */
453 10
    protected function readObject($path)
454
    {
455
        $options = [
456 10
            'Bucket' => $this->bucket,
457 10
            'Key'    => $this->applyPathPrefix($path),
458 5
        ];
459
460 10
        if (isset($this->options['@http'])) {
461 2
            $options['@http'] = $this->options['@http'];
462 1
        }
463
464 10
        $command = $this->s3Client->getCommand('getObject', $options + $this->options);
465
466
        try {
467
            /** @var Result $response */
468 10
            $response = $this->s3Client->execute($command);
469 6
        } catch (S3Exception $e) {
470 2
            return false;
471
        }
472
473 8
        return $this->normalizeResponse($response->toArray(), $path);
474
    }
475
476
    /**
477
     * Set the visibility for a file.
478
     *
479
     * @param string $path
480
     * @param string $visibility
481
     *
482
     * @return array|false file meta data
483
     */
484 6
    public function setVisibility($path, $visibility)
485
    {
486 6
        $command = $this->s3Client->getCommand(
487 6
            'putObjectAcl',
488
            [
489 6
                'Bucket' => $this->bucket,
490 6
                'Key'    => $this->applyPathPrefix($path),
491 6
                'ACL'    => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
492
            ]
493 3
        );
494
495
        try {
496 6
            $this->s3Client->execute($command);
497 4
        } catch (S3Exception $exception) {
498 2
            return false;
499
        }
500
501 4
        return compact('path', 'visibility');
502
    }
503
504
    /**
505
     * Get the visibility of a file.
506
     *
507
     * @param string $path
508
     *
509
     * @return array|false
510
     */
511 4
    public function getVisibility($path)
512
    {
513 4
        return ['visibility' => $this->getRawVisibility($path)];
514
    }
515
516
    /**
517
     * {@inheritdoc}
518
     */
519 68
    public function applyPathPrefix($path)
520
    {
521 68
        return ltrim(parent::applyPathPrefix($path), '/');
522
    }
523
524
    /**
525
     * {@inheritdoc}
526
     */
527 76
    public function setPathPrefix($prefix)
528
    {
529 76
        $prefix = ltrim($prefix, '/');
530
531 76
        return parent::setPathPrefix($prefix);
532
    }
533
534
    /**
535
     * Get the object acl presented as a visibility.
536
     *
537
     * @param string $path
538
     *
539
     * @return string
540
     */
541 12
    protected function getRawVisibility($path)
542
    {
543 12
        $command = $this->s3Client->getCommand(
544 12
            'getObjectAcl',
545
            [
546 12
                'Bucket' => $this->bucket,
547 12
                'Key'    => $this->applyPathPrefix($path),
548
            ]
549 6
        );
550
551 12
        $result = $this->s3Client->execute($command);
552 12
        $visibility = AdapterInterface::VISIBILITY_PRIVATE;
553
554 12
        foreach ($result->get('Grants') as $grant) {
555
            if (
556 2
                isset($grant['Grantee']['URI'])
557 2
                && $grant['Grantee']['URI'] === self::PUBLIC_GRANT_URI
558 2
                && $grant['Permission'] === 'READ'
559 1
            ) {
560 2
                $visibility = AdapterInterface::VISIBILITY_PUBLIC;
561 2
                break;
562
            }
563 6
        }
564
565 12
        return $visibility;
566
    }
567
568
    /**
569
     * Upload an object.
570
     *
571
     * @param        $path
572
     * @param        $body
573
     * @param Config $config
574
     *
575
     * @return array|bool
576
     */
577 14
    protected function upload($path, $body, Config $config)
578
    {
579 14
        $key = $this->applyPathPrefix($path);
580 14
        $options = $this->getOptionsFromConfig($config);
581 14
        $acl = array_key_exists('ACL', $options) ? $options['ACL'] : 'private';
582
583 14
        if ( ! isset($options['ContentType'])) {
584 4
            $options['ContentType'] = Util::guessMimeType($path, $body);
585 2
        }
586
587 14
        if ( ! isset($options['ContentLength'])) {
588 14
            $options['ContentLength'] = is_string($body) ? Util::contentSize($body) : Util::getStreamSize($body);
589 7
        }
590
591 14
        if ($options['ContentLength'] === null) {
592
            unset($options['ContentLength']);
593
        }
594
595
        try {
596 14
            $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
597 8
        } catch (S3MultipartUploadException $multipartUploadException) {
598 2
            return false;
599
        }
600
601 12
        return $this->normalizeResponse($options, $path);
602
    }
603
604
    /**
605
     * Get options from the config.
606
     *
607
     * @param Config $config
608
     *
609
     * @return array
610
     */
611 14
    protected function getOptionsFromConfig(Config $config)
612
    {
613 14
        $options = $this->options;
614
615 14
        if ($visibility = $config->get('visibility')) {
616
            // For local reference
617 10
            $options['visibility'] = $visibility;
618
            // For external reference
619 10
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
620 5
        }
621
622 14
        if ($mimetype = $config->get('mimetype')) {
623
            // For local reference
624 10
            $options['mimetype'] = $mimetype;
625
            // For external reference
626 10
            $options['ContentType'] = $mimetype;
627 5
        }
628
629 14
        foreach (static::$metaOptions as $option) {
630 14
            if ( ! $config->has($option)) {
631 14
                continue;
632
            }
633 10
            $options[$option] = $config->get($option);
634 7
        }
635
636 14
        return $options;
637
    }
638
639
    /**
640
     * Normalize the object result array.
641
     *
642
     * @param array  $response
643
     * @param string $path
644
     *
645
     * @return array
646
     */
647 28
    protected function normalizeResponse(array $response, $path = null)
648
    {
649
        $result = [
650 28
            'path' => $path ?: $this->removePathPrefix(
651 16
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
652 14
            ),
653 14
        ];
654 28
        $result = array_merge($result, Util::pathinfo($result['path']));
655
656 28
        if (isset($response['LastModified'])) {
657 16
            $result['timestamp'] = strtotime($response['LastModified']);
658 8
        }
659
660 28
        if (substr($result['path'], -1) === '/') {
661 6
            $result['type'] = 'dir';
662 6
            $result['path'] = rtrim($result['path'], '/');
663
664 6
            return $result;
665
        }
666
667 26
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
668
    }
669
670
    /**
671
     * @param $location
672
     *
673
     * @return bool
674
     */
675 10
    protected function doesDirectoryExist($location)
676
    {
677
        // Maybe this isn't an actual key, but a prefix.
678
        // Do a prefix listing of objects to determine.
679 10
        $command = $this->s3Client->getCommand(
680 10
            'listObjects',
681
            [
682 10
                'Bucket'  => $this->bucket,
683 10
                'Prefix'  => rtrim($location, '/') . '/',
684 10
                'MaxKeys' => 1,
685
            ]
686 5
        );
687
688
        try {
689 10
            $result = $this->s3Client->execute($command);
690
691 6
            return $result['Contents'] || $result['CommonPrefixes'];
692 4
        } catch (S3Exception $e) {
693 4
            if ($e->getStatusCode() === 403) {
694 2
                return false;
695
            }
696
697 2
            throw $e;
698
        }
699
    }
700
}
701