Completed
Push — master ( d9c27e...a7ff1d )
by Frank
10s
created

AwsS3Adapter::copy()   B

Complexity

Conditions 4
Paths 8

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

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