Completed
Push — master ( 9091a0...a2737a )
by Frank
8s
created

AwsS3Adapter::delete()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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