Completed
Pull Request — master (#143)
by
unknown
07:34
created

AwsS3Adapter::getTimestamp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
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
        'StorageClass' => 'storageclass',
29
    ];
30
31
    /**
32
     * @var array
33
     */
34
    protected static $metaOptions = [
35
        'ACL',
36
        'CacheControl',
37
        'ContentDisposition',
38
        'ContentEncoding',
39
        'ContentLength',
40
        'ContentType',
41
        'Expires',
42
        'GrantFullControl',
43
        'GrantRead',
44
        'GrantReadACP',
45
        'GrantWriteACP',
46
        'Metadata',
47
        'RequestPayer',
48
        'SSECustomerAlgorithm',
49
        'SSECustomerKey',
50
        'SSECustomerKeyMD5',
51
        'SSEKMSKeyId',
52
        'ServerSideEncryption',
53
        'StorageClass',
54
        'Tagging',
55
        'WebsiteRedirectLocation'
56
    ];
57
58
    /**
59
     * @var S3Client
60
     */
61
    protected $s3Client;
62
63
    /**
64
     * @var string
65
     */
66
    protected $bucket;
67
68
    /**
69
     * @var array
70
     */
71
    protected $options = [];
72
73
    /**
74
     * Constructor.
75
     *
76
     * @param S3Client $client
77
     * @param string   $bucket
78
     * @param string   $prefix
79
     * @param array    $options
80
     */
81 72
    public function __construct(S3Client $client, $bucket, $prefix = '', array $options = [])
82
    {
83 72
        $this->s3Client = $client;
84 72
        $this->bucket = $bucket;
85 72
        $this->setPathPrefix($prefix);
86 72
        $this->options = $options;
87 72
    }
88
89
    /**
90
     * Get the S3Client bucket.
91
     *
92
     * @return string
93
     */
94 4
    public function getBucket()
95
    {
96 4
        return $this->bucket;
97
    }
98
99
    /**
100
     * Set the S3Client bucket.
101
     *
102
     * @return string
103
     */
104 2
    public function setBucket($bucket)
105
    {
106 2
        $this->bucket =  $bucket;
107 2
    }
108
109
    /**
110
     * Get the S3Client instance.
111
     *
112
     * @return S3Client
113
     */
114 2
    public function getClient()
115
    {
116 2
        return $this->s3Client;
117
    }
118
119
    /**
120
     * Write a new file.
121
     *
122
     * @param string $path
123
     * @param string $contents
124
     * @param Config $config Config object
125
     *
126
     * @return false|array false on failure file meta data on success
127
     */
128 2
    public function write($path, $contents, Config $config)
129
    {
130 2
        return $this->upload($path, $contents, $config);
131
    }
132
133
    /**
134
     * Update a file.
135
     *
136
     * @param string $path
137
     * @param string $contents
138
     * @param Config $config Config object
139
     *
140
     * @return false|array false on failure file meta data on success
141
     */
142 2
    public function update($path, $contents, Config $config)
143
    {
144 2
        return $this->upload($path, $contents, $config);
145
    }
146
147
    /**
148
     * Rename a file.
149
     *
150
     * @param string $path
151
     * @param string $newpath
152
     *
153
     * @return bool
154
     */
155 4
    public function rename($path, $newpath)
156
    {
157 4
        if ( ! $this->copy($path, $newpath)) {
158 2
            return false;
159
        }
160
161 2
        return $this->delete($path);
162
    }
163
164
    /**
165
     * Delete a file.
166
     *
167
     * @param string $path
168
     *
169
     * @return bool
170
     */
171 5
    public function delete($path)
172
    {
173 4
        $location = $this->applyPathPrefix($path);
174
175 4
        $command = $this->s3Client->getCommand(
176 4
            'deleteObject',
177
            [
178 4
                'Bucket' => $this->bucket,
179 4
                'Key' => $location,
180
            ]
181 2
        );
182
183 4
        $this->s3Client->execute($command);
184
185 5
        return ! $this->has($path);
186
    }
187
188
    /**
189
     * Delete a directory.
190
     *
191
     * @param string $dirname
192
     *
193
     * @return bool
194
     */
195 4
    public function deleteDir($dirname)
196
    {
197
        try {
198 4
            $prefix = $this->applyPathPrefix($dirname) . '/';
199 4
            $this->s3Client->deleteMatchingObjects($this->bucket, $prefix);
200 3
        } catch (DeleteMultipleObjectsException $exception) {
201 2
            return false;
202
        }
203
204 2
        return true;
205
    }
206
207
    /**
208
     * Create a directory.
209
     *
210
     * @param string $dirname directory name
211
     * @param Config $config
212
     *
213
     * @return bool|array
214
     */
215 2
    public function createDir($dirname, Config $config)
216
    {
217 2
        return $this->upload($dirname . '/', '', $config);
218
    }
219
220
    /**
221
     * Check whether a file exists.
222
     *
223
     * @param string $path
224
     *
225
     * @return bool
226
     */
227 12
    public function has($path)
228
    {
229 12
        $location = $this->applyPathPrefix($path);
230
231 12
        if ($this->s3Client->doesObjectExist($this->bucket, $location, $this->options)) {
232 2
            return true;
233
        }
234
235 10
        return $this->doesDirectoryExist($location);
236
    }
237
238
    /**
239
     * Read a file.
240
     *
241
     * @param string $path
242
     *
243
     * @return false|array
244
     */
245 4
    public function read($path)
246
    {
247 4
        $response = $this->readObject($path);
248
249 4
        if ($response !== false) {
250 2
            $response['contents'] = $response['contents']->getContents();
251 1
        }
252
253 4
        return $response;
254
    }
255
256
    /**
257
     * List contents of a directory.
258
     *
259
     * @param string $directory
260
     * @param bool   $recursive
261
     *
262
     * @return array
263
     */
264 2
    public function listContents($directory = '', $recursive = false)
265
    {
266 2
        $prefix = $this->applyPathPrefix(rtrim($directory, '/') . '/');
267 2
        $options = ['Bucket' => $this->bucket, 'Prefix' => ltrim($prefix, '/')];
268
269 2
        if ($recursive === false) {
270 2
            $options['Delimiter'] = '/';
271 1
        }
272
273 2
        $listing = $this->retrievePaginatedListing($options);
274 2
        $normalizer = [$this, 'normalizeResponse'];
275 2
        $normalized = array_map($normalizer, $listing);
276
277 2
        return Util::emulateDirectories($normalized);
278
    }
279
280
    /**
281
     * @param array $options
282
     *
283
     * @return array
284
     */
285 2
    protected function retrievePaginatedListing(array $options)
286
    {
287 2
        $resultPaginator = $this->s3Client->getPaginator('ListObjects', $options);
288 2
        $listing = [];
289
290 2
        foreach ($resultPaginator as $result) {
291
            $listing = array_merge($listing, $result->get('Contents') ?: [], $result->get('CommonPrefixes') ?: []);
292 1
        }
293
294 2
        return $listing;
295
    }
296
297
    /**
298
     * Get all the meta data of a file or directory.
299
     *
300
     * @param string $path
301
     *
302
     * @return false|array
303
     */
304 12
    public function getMetadata($path)
305
    {
306 12
        $command = $this->s3Client->getCommand(
307 12
            'headObject',
308
            [
309 12
                'Bucket' => $this->bucket,
310 12
                'Key' => $this->applyPathPrefix($path),
311 12
            ] + $this->options
312 6
        );
313
314
        /* @var Result $result */
315
        try {
316 12
            $result = $this->s3Client->execute($command);
317 8
        } catch (S3Exception $exception) {
318 4
            $response = $exception->getResponse();
319
320 4
            if ($response !== null && $response->getStatusCode() === 404) {
321 2
                return false;
322
            }
323
324 2
            throw $exception;
325
        }
326
327 8
        return $this->normalizeResponse($result->toArray(), $path);
328
    }
329
330
    /**
331
     * Get all the meta data of a file or directory.
332
     *
333
     * @param string $path
334
     *
335
     * @return false|array
336
     */
337 2
    public function getSize($path)
338
    {
339 2
        return $this->getMetadata($path);
340
    }
341
342
    /**
343
     * Get the mimetype of a file.
344
     *
345
     * @param string $path
346
     *
347
     * @return false|array
348
     */
349 2
    public function getMimetype($path)
350
    {
351 2
        return $this->getMetadata($path);
352
    }
353
354
    /**
355
     * Get the timestamp of a file.
356
     *
357
     * @param string $path
358
     *
359
     * @return false|array
360
     */
361 2
    public function getTimestamp($path)
362
    {
363 2
        return $this->getMetadata($path);
364
    }
365
366
    /**
367
     * Write a new file using a stream.
368
     *
369
     * @param string   $path
370
     * @param resource $resource
371
     * @param Config   $config Config object
372
     *
373
     * @return array|false false on failure file meta data on success
374
     */
375 2
    public function writeStream($path, $resource, Config $config)
376
    {
377 2
        return $this->upload($path, $resource, $config);
378
    }
379
380
    /**
381
     * Update a file using a stream.
382
     *
383
     * @param string   $path
384
     * @param resource $resource
385
     * @param Config   $config Config object
386
     *
387
     * @return array|false false on failure file meta data on success
388
     */
389 2
    public function updateStream($path, $resource, Config $config)
390
    {
391 2
        return $this->upload($path, $resource, $config);
392
    }
393
394
    /**
395
     * Copy a file.
396
     *
397
     * @param string $path
398
     * @param string $newpath
399
     *
400
     * @return bool
401
     */
402 8
    public function copy($path, $newpath)
403
    {
404 8
        $command = $this->s3Client->getCommand(
405 8
            'copyObject',
406
            [
407 8
                'Bucket' => $this->bucket,
408 8
                'Key' => $this->applyPathPrefix($newpath),
409 8
                'CopySource' => urlencode($this->bucket . '/' . $this->applyPathPrefix($path)),
410 8
                'ACL' => $this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC
411 8
                    ? 'public-read' : 'private',
412 8
            ] + $this->options
413 4
        );
414
415
        try {
416 8
            $this->s3Client->execute($command);
417 6
        } 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 4
    public function readStream($path)
432
    {
433 4
        $response = $this->readObject($path);
434
435 4
        if ($response !== false) {
436 4
            $response['stream'] = $response['contents']->detach();
437 4
            unset($response['contents']);
438 2
        }
439
440 4
        return $response;
441
    }
442
443
    /**
444
     * Read an object and normalize the response.
445
     *
446
     * @param $path
447
     *
448
     * @return array|bool
449
     */
450 8
    protected function readObject($path)
451
    {
452
        $options = [
453 8
            'Bucket' => $this->bucket,
454 8
            'Key' => $this->applyPathPrefix($path),
455 4
        ];
456
457 8
        if (isset($this->options['@http'])) {
458 2
            $options['@http'] = $this->options['@http'];
459 1
        }
460
461 8
        $command = $this->s3Client->getCommand('getObject', $options + $this->options);
462
463
        try {
464
            /** @var Result $response */
465 8
            $response = $this->s3Client->execute($command);
466 5
        } catch (S3Exception $e) {
467 2
            return false;
468
        }
469
470 6
        return $this->normalizeResponse($response->toArray(), $path);
471
    }
472
473
    /**
474
     * Set the visibility for a file.
475
     *
476
     * @param string $path
477
     * @param string $visibility
478
     *
479
     * @return array|false file meta data
480
     */
481 6
    public function setVisibility($path, $visibility)
482
    {
483 6
        $command = $this->s3Client->getCommand(
484 6
            'putObjectAcl',
485
            [
486 6
                'Bucket' => $this->bucket,
487 6
                'Key' => $this->applyPathPrefix($path),
488 6
                'ACL' => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
489
            ]
490 3
        );
491
492
        try {
493 6
            $this->s3Client->execute($command);
494 4
        } catch (S3Exception $exception) {
495 2
            return false;
496
        }
497
498 4
        return compact('path', 'visibility');
499
    }
500
501
    /**
502
     * Get the visibility of a file.
503
     *
504
     * @param string $path
505
     *
506
     * @return array|false
507
     */
508 4
    public function getVisibility($path)
509
    {
510 4
        return ['visibility' => $this->getRawVisibility($path)];
511
    }
512
513
    /**
514
     * {@inheritdoc}
515
     */
516 64
    public function applyPathPrefix($path)
517
    {
518 64
        return ltrim(parent::applyPathPrefix($path), '/');
519
    }
520
521
    /**
522
     * {@inheritdoc}
523
     */
524 72
    public function setPathPrefix($prefix)
525
    {
526 72
        $prefix = ltrim($prefix, '/');
527
528 72
        return parent::setPathPrefix($prefix);
529
    }
530
    
531
    /**
532
     * Get a presigned Url for a file.
533
     *
534
     * @param string $path
535
     * @param string $expiration
536
     *
537
     * @return string
538
     */
539
    public function getPresignedUrl($path,$expiration = "+20 minutes")
540
    {
541
        $options = [
542
            'Bucket' => $this->bucket,
543
            'Key' => $this->applyPathPrefix($path),
544
        ];
545
546
        if (isset($this->options['@http'])) {
547
            $options['@http'] = $this->options['@http'];
548
        }
549
550
        $command = $this->s3Client->getCommand('getObject', $options + $this->options);
551
552
        try {
553
            $request = $s3Client->createPresignedRequest($command, $expiration);
0 ignored issues
show
Bug introduced by
The variable $s3Client does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
554
            return (string) $request->getUri();
555
        } catch (S3Exception $exception) {
556
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by League\Flysystem\AwsS3v3...dapter::getPresignedUrl of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
557
        }
558
        
559
        return false;
0 ignored issues
show
Unused Code introduced by
return false; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
560
    }
561
562
563
    /**
564
     * Get the object acl presented as a visibility.
565
     *
566
     * @param string $path
567
     *
568
     * @return string
569
     */
570 12
    protected function getRawVisibility($path)
571
    {
572 12
        $command = $this->s3Client->getCommand(
573 12
            'getObjectAcl',
574
            [
575 12
                'Bucket' => $this->bucket,
576 12
                'Key' => $this->applyPathPrefix($path),
577
            ]
578 6
        );
579
580 12
        $result = $this->s3Client->execute($command);
581 12
        $visibility = AdapterInterface::VISIBILITY_PRIVATE;
582
583 12
        foreach ($result->get('Grants') as $grant) {
584
            if (
585 2
                isset($grant['Grantee']['URI'])
586 2
                && $grant['Grantee']['URI'] === self::PUBLIC_GRANT_URI
587 2
                && $grant['Permission'] === 'READ'
588 1
            ) {
589 2
                $visibility = AdapterInterface::VISIBILITY_PUBLIC;
590 2
                break;
591
            }
592 6
        }
593
594 12
        return $visibility;
595
    }
596
597
    /**
598
     * Upload an object.
599
     *
600
     * @param        $path
601
     * @param        $body
602
     * @param Config $config
603
     *
604
     * @return array
605
     */
606 10
    protected function upload($path, $body, Config $config)
607
    {
608 10
        $key = $this->applyPathPrefix($path);
609 10
        $options = $this->getOptionsFromConfig($config);
610 10
        $acl = array_key_exists('ACL', $options) ? $options['ACL'] : 'private';
611
612 10
        if ( ! isset($options['ContentType'])) {
613 2
            $options['ContentType'] = Util::guessMimeType($path, $body);
614 1
        }
615
616 10
        if ( ! isset($options['ContentLength'])) {
617 10
            $options['ContentLength'] = is_string($body) ? Util::contentSize($body) : Util::getStreamSize($body);
618 5
        }
619
620 10
        if ($options['ContentLength'] === null) {
621
            unset($options['ContentLength']);
622
        }
623
624 10
        $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
625
626 10
        return $this->normalizeResponse($options, $key);
627
    }
628
629
    /**
630
     * Get options from the config.
631
     *
632
     * @param Config $config
633
     *
634
     * @return array
635
     */
636 10
    protected function getOptionsFromConfig(Config $config)
637
    {
638 10
        $options = $this->options;
639
640 10
        if ($visibility = $config->get('visibility')) {
641
            // For local reference
642 8
            $options['visibility'] = $visibility;
643
            // For external reference
644 8
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
645 4
        }
646
647 10
        if ($mimetype = $config->get('mimetype')) {
648
            // For local reference
649 8
            $options['mimetype'] = $mimetype;
650
            // For external reference
651 8
            $options['ContentType'] = $mimetype;
652 4
        }
653
654 10
        foreach (static::$metaOptions as $option) {
655 10
            if ( ! $config->has($option)) {
656 10
                continue;
657
            }
658 8
            $options[$option] = $config->get($option);
659 5
        }
660
661 10
        return $options;
662
    }
663
664
    /**
665
     * Normalize the object result array.
666
     *
667
     * @param array  $response
668
     * @param string $path
669
     *
670
     * @return array
671
     */
672 24
    protected function normalizeResponse(array $response, $path = null)
673
    {
674
        $result = [
675 24
            'path' => $path ?: $this->removePathPrefix(
676 12
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
677 12
            ),
678 12
        ];
679 24
        $result = array_merge($result, Util::pathinfo($result['path']));
680
681 24
        if (isset($response['LastModified'])) {
682 14
            $result['timestamp'] = strtotime($response['LastModified']);
683 7
        }
684
685 24
        if (substr($result['path'], -1) === '/') {
686 2
            $result['type'] = 'dir';
687 2
            $result['path'] = rtrim($result['path'], '/');
688
689 2
            return $result;
690
        }
691
692 22
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
693
    }
694
695
    /**
696
     * @param $location
697
     *
698
     * @return bool
699
     */
700 10
    protected function doesDirectoryExist($location)
701
    {
702
        // Maybe this isn't an actual key, but a prefix.
703
        // Do a prefix listing of objects to determine.
704 10
        $command = $this->s3Client->getCommand(
705 10
            'listObjects',
706
            [
707 10
                'Bucket' => $this->bucket,
708 10
                'Prefix' => rtrim($location, '/') . '/',
709 10
                'MaxKeys' => 1,
710
            ]
711 5
        );
712
713
        try {
714 10
            $result = $this->s3Client->execute($command);
715
716 6
            return $result['Contents'] || $result['CommonPrefixes'];
717 4
        } catch (S3Exception $e) {
718 4
            if ($e->getStatusCode() === 403) {
719 2
                return false;
720
            }
721
722 2
            throw $e;
723
        }
724
    }
725
}
726