Completed
Pull Request — master (#133)
by
unknown
04:25
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
        'ACL',
35
        'CacheControl',
36
        'ContentDisposition',
37
        'ContentEncoding',
38
        'ContentLength',
39
        'ContentType',
40
        'Expires',
41
        'GrantFullControl',
42
        'GrantRead',
43
        'GrantReadACP',
44
        'GrantWriteACP',
45
        'Metadata',
46
        'RequestPayer',
47
        'SSECustomerAlgorithm',
48
        'SSECustomerKey',
49
        'SSECustomerKeyMD5',
50
        'SSEKMSKeyId',
51
        'ServerSideEncryption',
52
        'StorageClass',
53
        'Tagging',
54
        'WebsiteRedirectLocation'
55
    ];
56
57
    /**
58
     * @var S3Client
59
     */
60
    protected $s3Client;
61
62
    /**
63
     * @var string
64
     */
65
    protected $bucket;
66
67
    /**
68
     * @var array
69
     */
70
    protected $options = [];
71
72
    /**
73
     * Constructor.
74
     *
75
     * @param S3Client $client
76
     * @param string   $bucket
77
     * @param string   $prefix
78
     * @param array    $options
79
     */
80 72
    public function __construct(S3Client $client, $bucket, $prefix = '', array $options = [])
81
    {
82 72
        $this->s3Client = $client;
83 72
        $this->bucket = $bucket;
84 72
        $this->setPathPrefix($prefix);
85 72
        $this->options = $options;
86 72
    }
87
88
    /**
89
     * Get the S3Client bucket.
90
     *
91
     * @return string
92
     */
93 4
    public function getBucket()
94
    {
95 4
        return $this->bucket;
96
    }
97
98
    /**
99
     * Set the S3Client bucket.
100
     *
101
     * @return string
102
     */
103 2
    public function setBucket($bucket)
104
    {
105 2
        $this->bucket =  $bucket;
106 2
    }
107
108
    /**
109
     * Get the S3Client instance.
110
     *
111
     * @return S3Client
112
     */
113 2
    public function getClient()
114
    {
115 2
        return $this->s3Client;
116
    }
117
118
    /**
119
     * Write a new file.
120
     *
121
     * @param string $path
122
     * @param string $contents
123
     * @param Config $config Config object
124
     *
125
     * @return false|array false on failure file meta data on success
126
     */
127 2
    public function write($path, $contents, Config $config)
128
    {
129 2
        return $this->upload($path, $contents, $config);
130
    }
131
132
    /**
133
     * Update a file.
134
     *
135
     * @param string $path
136
     * @param string $contents
137
     * @param Config $config Config object
138
     *
139
     * @return false|array false on failure file meta data on success
140
     */
141 2
    public function update($path, $contents, Config $config)
142
    {
143 2
        return $this->upload($path, $contents, $config);
144
    }
145
146
    /**
147
     * Rename a file.
148
     *
149
     * @param string $path
150
     * @param string $newpath
151
     *
152
     * @return bool
153
     */
154 4
    public function rename($path, $newpath)
155
    {
156 4
        if ( ! $this->copy($path, $newpath)) {
157 2
            return false;
158
        }
159
160 2
        return $this->delete($path);
161
    }
162
163
    /**
164
     * Delete a file.
165
     *
166
     * @param string $path
167
     *
168
     * @return bool
169
     */
170 4
    public function delete($path)
171
    {
172 4
        $location = $this->applyPathPrefix($path);
173
174 4
        $command = $this->s3Client->getCommand(
175 4
            'deleteObject',
176
            [
177 4
                'Bucket' => $this->bucket,
178 4
                'Key' => $location,
179 1
            ]
180 2
        );
181
182 4
        $this->s3Client->execute($command);
183
184 4
        return ! $this->has($path);
185
    }
186
187
    /**
188
     * Delete a directory.
189
     *
190
     * @param string $dirname
191
     *
192
     * @return bool
193
     */
194 4
    public function deleteDir($dirname)
195
    {
196
        try {
197 4
            $prefix = $this->applyPathPrefix($dirname) . '/';
198 4
            $this->s3Client->deleteMatchingObjects($this->bucket, $prefix);
199 3
        } catch (DeleteMultipleObjectsException $exception) {
200 2
            return false;
201
        }
202
203 2
        return true;
204
    }
205
206
    /**
207
     * Create a directory.
208
     *
209
     * @param string $dirname directory name
210
     * @param Config $config
211
     *
212
     * @return bool|array
213
     */
214 2
    public function createDir($dirname, Config $config)
215
    {
216 2
        return $this->upload($dirname . '/', '', $config);
217
    }
218
219
    /**
220
     * Check whether a file exists.
221
     *
222
     * @param string $path
223
     *
224
     * @return bool
225
     */
226 12
    public function has($path)
227
    {
228 12
        $location = $this->applyPathPrefix($path);
229
230 12
        if ($this->s3Client->doesObjectExist($this->bucket, $location, $this->options)) {
231 2
            return true;
232
        }
233
234 10
        return $this->doesDirectoryExist($location);
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 1
        }
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 1
        }
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 1
        }
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 12
            ] + $this->options
311 6
        );
312
313
        /* @var Result $result */
314
        try {
315 12
            $result = $this->s3Client->execute($command);
316 8
        } 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
        $command = $this->s3Client->getCommand(
404 8
            'copyObject',
405
            [
406 8
                'Bucket' => $this->bucket,
407 8
                'Key' => $this->applyPathPrefix($newpath),
408 8
                'CopySource' => urlencode($this->bucket . '/' . $this->applyPathPrefix($path)),
409 8
                'ACL' => $this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC
410 8
                    ? 'public-read' : 'private',
411 8
            ] + $this->options
412 4
        );
413
414
        try {
415 8
            $this->s3Client->execute($command);
416 6
        } catch (S3Exception $e) {
417 4
            return false;
418
        }
419
420 4
        return true;
421
    }
422
423
    /**
424
     * Read a file as a stream.
425
     *
426
     * @param string $path
427
     *
428
     * @return array|false
429
     */
430 4
    public function readStream($path)
431
    {
432 4
        $response = $this->readObject($path);
433
434 4
        if ($response !== false) {
435 4
            $response['stream'] = $response['contents']->detach();
436 4
            unset($response['contents']);
437 2
        }
438
439 4
        return $response;
440
    }
441
442
    /**
443
     * Read an object and normalize the response.
444
     *
445
     * @param $path
446
     *
447
     * @return array|bool
448
     */
449 8
    protected function readObject($path)
450
    {
451
        $options = [
452 8
            'Bucket' => $this->bucket,
453 8
            'Key' => $this->applyPathPrefix($path),
454 4
        ];
455
456 8
        if (isset($this->options['@http'])) {
457 2
            $options['@http'] = $this->options['@http'];
458 1
        }
459
460 8
        $command = $this->s3Client->getCommand('getObject', $options + $this->options);
461
462
        try {
463
            /** @var Result $response */
464 8
            $response = $this->s3Client->execute($command);
465 5
        } catch (S3Exception $e) {
466 2
            return false;
467
        }
468
469 6
        return $this->normalizeResponse($response->toArray(), $path);
470
    }
471
472
    /**
473
     * Set the visibility for a file.
474
     *
475
     * @param string $path
476
     * @param string $visibility
477
     *
478
     * @return array|false file meta data
479
     */
480 6
    public function setVisibility($path, $visibility)
481
    {
482 6
        $command = $this->s3Client->getCommand(
483 6
            'putObjectAcl',
484
            [
485 6
                'Bucket' => $this->bucket,
486 6
                'Key' => $this->applyPathPrefix($path),
487 6
                'ACL' => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
488
            ]
489 3
        );
490
491
        try {
492 6
            $this->s3Client->execute($command);
493 4
        } catch (S3Exception $exception) {
494 2
            return false;
495
        }
496
497 4
        return compact('path', 'visibility');
498
    }
499
500
    /**
501
     * Get the visibility of a file.
502
     *
503
     * @param string $path
504
     *
505
     * @return array|false
506
     */
507 4
    public function getVisibility($path)
508
    {
509 4
        return ['visibility' => $this->getRawVisibility($path)];
510
    }
511
512
    /**
513
     * {@inheritdoc}
514
     */
515 64
    public function applyPathPrefix($path)
516
    {
517 64
        return ltrim(parent::applyPathPrefix($path), '/');
518
    }
519
520
    /**
521
     * {@inheritdoc}
522
     */
523 72
    public function setPathPrefix($prefix)
524
    {
525 72
        $prefix = ltrim($prefix, '/');
526
527 72
        return parent::setPathPrefix($prefix);
528
    }
529
530
    /**
531
     * Get the object acl presented as a visibility.
532
     *
533
     * @param string $path
534
     *
535
     * @return string
536
     */
537 12
    protected function getRawVisibility($path)
538
    {
539 12
        $command = $this->s3Client->getCommand(
540 12
            'getObjectAcl',
541
            [
542 12
                'Bucket' => $this->bucket,
543 12
                'Key' => $this->applyPathPrefix($path),
544
            ]
545 6
        );
546
547 12
        $result = $this->s3Client->execute($command);
548 12
        $visibility = AdapterInterface::VISIBILITY_PRIVATE;
549
550 12
        foreach ($result->get('Grants') as $grant) {
551
            if (
552 2
                isset($grant['Grantee']['URI'])
553 2
                && $grant['Grantee']['URI'] === self::PUBLIC_GRANT_URI
554 2
                && $grant['Permission'] === 'READ'
555 1
            ) {
556 2
                $visibility = AdapterInterface::VISIBILITY_PUBLIC;
557 2
                break;
558
            }
559 6
        }
560
561 12
        return $visibility;
562
    }
563
564
    /**
565
     * Upload an object.
566
     *
567
     * @param        $path
568
     * @param        $body
569
     * @param Config $config
570
     *
571
     * @return array
572
     */
573 10
    protected function upload($path, $body, Config $config)
574
    {
575 10
        $key = $this->applyPathPrefix($path);
576 10
        $options = $this->getOptionsFromConfig($config);
577 10
        $acl = array_key_exists('ACL', $options) ? $options['ACL'] : 'private';
578
579 10
        if ( ! isset($options['ContentType'])) {
580 2
            $options['ContentType'] = Util::guessMimeType($path, $body);
581 1
        }
582
583 10
        if ( ! isset($options['ContentLength'])) {
584 10
            $options['ContentLength'] = is_string($body) ? Util::contentSize($body) : Util::getStreamSize($body);
585 5
        }
586
587 10
        if ($options['ContentLength'] === null) {
588
            unset($options['ContentLength']);
589
        }
590
591 10
        $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
592
593 10
        return $this->normalizeResponse($options, $key);
594
    }
595
596
    /**
597
     * Get options from the config.
598
     *
599
     * @param Config $config
600
     *
601
     * @return array
602
     */
603 10
    protected function getOptionsFromConfig(Config $config)
604
    {
605 10
        $options = $this->options;
606
607 10
        if ($visibility = $config->get('visibility')) {
608
            // For local reference
609 8
            $options['visibility'] = $visibility;
610
            // For external reference
611 8
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
612 4
        }
613
614 10
        if ($mimetype = $config->get('mimetype')) {
615
            // For local reference
616 8
            $options['mimetype'] = $mimetype;
617
            // For external reference
618 8
            $options['ContentType'] = $mimetype;
619 4
        }
620
621 10
        foreach (static::$metaOptions as $option) {
622 10
            if ( ! $config->has($option)) {
623 10
                continue;
624
            }
625 8
            $options[$option] = $config->get($option);
626 5
        }
627
628 10
        return $options;
629
    }
630
631
    /**
632
     * Normalize the object result array.
633
     *
634
     * @param array  $response
635
     * @param string $path
636
     *
637
     * @return array
638
     */
639 24
    protected function normalizeResponse(array $response, $path = null)
640
    {
641
        $result = [
642 24
            'path' => $path ?: $this->removePathPrefix(
643 12
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
644 12
            ),
645 12
        ];
646 24
        $result = array_merge($result, Util::pathinfo($result['path']));
647
648 24
        if (isset($response['LastModified'])) {
649 14
            $result['timestamp'] = strtotime($response['LastModified']);
650 7
        }
651
652 24
        if (substr($result['path'], -1) === '/') {
653 2
            $result['type'] = 'dir';
654 2
            $result['path'] = rtrim($result['path'], '/');
655
656 2
            return $result;
657
        }
658
659 22
        if (isset($response['ETag'])) {
660
            $result['md5sum'] = $response['ETag'];
661
        }
662
663 22
        foreach (static::$metaOptions as $option) {
664 22
            if ( ! isset($response[$option]) || empty($response[$option])) {
665 22
                continue;
666
            }
667 16
            $result[$option] = $response[$option];
668 11
        }
669
670 22
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
671
    }
672
673
    /**
674
     * @param $location
675
     *
676
     * @return bool
677
     */
678 10
    protected function doesDirectoryExist($location)
679
    {
680
        // Maybe this isn't an actual key, but a prefix.
681
        // Do a prefix listing of objects to determine.
682 10
        $command = $this->s3Client->getCommand(
683 10
            'listObjects',
684
            [
685 10
                'Bucket' => $this->bucket,
686 10
                'Prefix' => rtrim($location, '/') . '/',
687 10
                'MaxKeys' => 1,
688
            ]
689 5
        );
690
691
        try {
692 10
            $result = $this->s3Client->execute($command);
693
694 6
            return $result['Contents'] || $result['CommonPrefixes'];
695 4
        } catch (S3Exception $e) {
696 4
            if ($e->getStatusCode() === 403) {
697 2
                return false;
698
            }
699
700 2
            throw $e;
701
        }
702
    }
703
}
704