Completed
Push — master ( b0b48f...14eadf )
by Frank
02:02
created

AwsS3Adapter::upload()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.0852

Importance

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