Completed
Pull Request — master (#152)
by
unknown
05:20
created

AwsS3Adapter::updateStream()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

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
nc 1
nop 3
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
        'ETag'          => 'etag',
30
        'VersionId'     => 'versionid'
31
    ];
32
33
    /**
34
     * @var array
35
     */
36
    protected static $metaOptions = [
37
        'ACL',
38
        'CacheControl',
39
        'ContentDisposition',
40
        'ContentEncoding',
41
        'ContentLength',
42
        'ContentType',
43
        'Expires',
44
        'GrantFullControl',
45
        'GrantRead',
46
        'GrantReadACP',
47
        'GrantWriteACP',
48
        'Metadata',
49
        'RequestPayer',
50
        'SSECustomerAlgorithm',
51
        'SSECustomerKey',
52
        'SSECustomerKeyMD5',
53
        'SSEKMSKeyId',
54
        'ServerSideEncryption',
55
        'StorageClass',
56
        'Tagging',
57
        'WebsiteRedirectLocation',
58
    ];
59
60
    /**
61
     * @var S3Client
62
     */
63
    protected $s3Client;
64
65
    /**
66
     * @var string
67
     */
68
    protected $bucket;
69
70
    /**
71
     * @var array
72
     */
73
    protected $options = [];
74
75
    /**
76
     * Constructor.
77
     *
78
     * @param S3Client $client
79
     * @param string   $bucket
80
     * @param string   $prefix
81
     * @param array    $options
82 72
     */
83
    public function __construct(S3Client $client, $bucket, $prefix = '', array $options = [])
84 72
    {
85 72
        $this->s3Client($client)
0 ignored issues
show
Bug introduced by
The method s3Client() does not seem to exist on object<League\Flysystem\AwsS3v3\AwsS3Adapter>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

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