Completed
Push — master ( 7645aa...b0b48f )
by Frank
03:04
created

AwsS3Adapter::upload()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.0208

Importance

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