Completed
Push — feature/etag-in-metadata ( 93724b )
by Frank
04:10
created

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