Completed
Push — master ( 2a20fa...e12738 )
by Frank
02:20
created

AwsS3Adapter::createDir()   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 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 2
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
    ];
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 62
    public function __construct(S3Client $client, $bucket, $prefix = '', array $options = [])
68
    {
69 62
        $this->s3Client = $client;
70 62
        $this->bucket = $bucket;
71 62
        $this->setPathPrefix($prefix);
72 62
        $this->options = $options;
73 62
    }
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 4
    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 4
        );
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 6
    public function has($path)
204
    {
205 6
        $location = $this->applyPathPrefix($path);
206
207 6
        return $this->s3Client->doesObjectExist($this->bucket, $location);
208
    }
209
210
    /**
211
     * Read a file.
212
     *
213
     * @param string $path
214
     *
215
     * @return false|array
216
     */
217 4
    public function read($path)
218
    {
219 4
        $response = $this->readObject($path);
220
221 4
        if ($response !== false) {
222 2
            $response['contents'] = $response['contents']->getContents();
223 2
        }
224
225 4
        return $response;
226
    }
227
228
    /**
229
     * List contents of a directory.
230
     *
231
     * @param string $directory
232
     * @param bool   $recursive
233
     *
234
     * @return array
235
     */
236 2
    public function listContents($directory = '', $recursive = false)
237
    {
238 2
        $prefix = $this->applyPathPrefix(rtrim($directory, '/') . '/');
239 2
        $options = ['Bucket' => $this->bucket, 'Prefix' => ltrim($prefix, '/')];
240
241 2
        if ($recursive === false) {
242 2
            $options['Delimiter'] = '/';
243 2
        }
244
245 2
        $listing = $this->retrievePaginatedListing($options);
246 2
        $normalizer = [$this, 'normalizeResponse'];
247 2
        $normalized = array_map($normalizer, $listing);
248
249 2
        return Util::emulateDirectories($normalized);
250
    }
251
252
    /**
253
     * @param array $options
254
     *
255
     * @return array
256
     */
257 2
    protected function retrievePaginatedListing(array $options)
258
    {
259 2
        $resultPaginator = $this->s3Client->getPaginator('ListObjects', $options);
260 2
        $listing = [];
261
262 2
        foreach ($resultPaginator as $result) {
263
            $listing = array_merge($listing, $result->get('Contents') ?: [], $result->get('CommonPrefixes') ?: []);
264 2
        }
265
266 2
        return $listing;
267
    }
268
269
    /**
270
     * Get all the meta data of a file or directory.
271
     *
272
     * @param string $path
273
     *
274
     * @return false|array
275
     */
276 12
    public function getMetadata($path)
277
    {
278 12
        $command = $this->s3Client->getCommand(
279 12
            'headObject',
280
            [
281 12
                'Bucket' => $this->bucket,
282 12
                'Key' => $this->applyPathPrefix($path),
283
            ]
284 12
        );
285
286
        /* @var Result $result */
287
        try {
288 12
            $result = $this->s3Client->execute($command);
289 12
        } catch (S3Exception $exception) {
290 4
            $response = $exception->getResponse();
291
292 4
            if ($response !== null && $response->getStatusCode() === 404) {
293 2
                return false;
294
            }
295
296 2
            throw $exception;
297
        }
298
299 8
        return $this->normalizeResponse($result->toArray(), $path);
300
    }
301
302
    /**
303
     * Get all the meta data of a file or directory.
304
     *
305
     * @param string $path
306
     *
307
     * @return false|array
308
     */
309 2
    public function getSize($path)
310
    {
311 2
        return $this->getMetadata($path);
312
    }
313
314
    /**
315
     * Get the mimetype of a file.
316
     *
317
     * @param string $path
318
     *
319
     * @return false|array
320
     */
321 2
    public function getMimetype($path)
322
    {
323 2
        return $this->getMetadata($path);
324
    }
325
326
    /**
327
     * Get the timestamp of a file.
328
     *
329
     * @param string $path
330
     *
331
     * @return false|array
332
     */
333 2
    public function getTimestamp($path)
334
    {
335 2
        return $this->getMetadata($path);
336
    }
337
338
    /**
339
     * Write a new file using a stream.
340
     *
341
     * @param string   $path
342
     * @param resource $resource
343
     * @param Config   $config Config object
344
     *
345
     * @return array|false false on failure file meta data on success
346
     */
347 2
    public function writeStream($path, $resource, Config $config)
348
    {
349 2
        return $this->upload($path, $resource, $config);
350
    }
351
352
    /**
353
     * Update a file using a stream.
354
     *
355
     * @param string   $path
356
     * @param resource $resource
357
     * @param Config   $config Config object
358
     *
359
     * @return array|false false on failure file meta data on success
360
     */
361 2
    public function updateStream($path, $resource, Config $config)
362
    {
363 2
        return $this->upload($path, $resource, $config);
364
    }
365
366
    /**
367
     * Copy a file.
368
     *
369
     * @param string $path
370
     * @param string $newpath
371
     *
372
     * @return bool
373
     */
374 8
    public function copy($path, $newpath)
375
    {
376 8
        $visibility = $this->getRawVisibility($path);
377
378 8
        $command = $this->s3Client->getCommand(
379 8
            'copyObject',
380
            [
381 8
                'Bucket' => $this->bucket,
382 8
                'Key' => $this->applyPathPrefix($newpath),
383 8
                'CopySource' => urlencode($this->bucket . '/' . $this->applyPathPrefix($path)),
384 8
                'ACL' => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
385
            ]
386 8
        );
387
388
        try {
389 8
            $this->s3Client->execute($command);
390 8
        } catch (S3Exception $e) {
391 4
            return false;
392
        }
393
394 4
        return true;
395
    }
396
397
    /**
398
     * Read a file as a stream.
399
     *
400
     * @param string $path
401
     *
402
     * @return array|false
403
     */
404 2
    public function readStream($path)
405
    {
406 2
        $response = $this->readObject($path);
407
408 2
        if ($response !== false) {
409 2
            $response['stream'] = $response['contents']->detach();
410 2
            rewind($response['stream']);
411 2
            unset($response['contents']);
412 2
        }
413
414 2
        return $response;
415
    }
416
417
    /**
418
     * Read an object and normalize the response.
419
     *
420
     * @param $path
421
     *
422
     * @return array|bool
423
     */
424 6
    protected function readObject($path)
425
    {
426 6
        $command = $this->s3Client->getCommand(
427 6
            'getObject',
428
            [
429 6
                'Bucket' => $this->bucket,
430 6
                'Key' => $this->applyPathPrefix($path),
431
            ]
432 6
        );
433
434
        try {
435
            /** @var Result $response */
436 6
            $response = $this->s3Client->execute($command);
437 6
        } catch (S3Exception $e) {
438 2
            return false;
439
        }
440
441 4
        return $this->normalizeResponse($response->toArray(), $path);
442
    }
443
444
    /**
445
     * Set the visibility for a file.
446
     *
447
     * @param string $path
448
     * @param string $visibility
449
     *
450
     * @return array|false file meta data
451
     */
452 6
    public function setVisibility($path, $visibility)
453
    {
454 6
        $command = $this->s3Client->getCommand(
455 6
            'putObjectAcl',
456
            [
457 6
                'Bucket' => $this->bucket,
458 6
                'Key' => $this->applyPathPrefix($path),
459 6
                'ACL' => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
460
            ]
461 6
        );
462
463
        try {
464 6
            $this->s3Client->execute($command);
465 6
        } catch (S3Exception $exception) {
466 2
            return false;
467
        }
468
469 4
        return compact('path', 'visibility');
470
    }
471
472
    /**
473
     * Get the visibility of a file.
474
     *
475
     * @param string $path
476
     *
477
     * @return array|false
478
     */
479 4
    public function getVisibility($path)
480
    {
481 4
        return ['visibility' => $this->getRawVisibility($path)];
482
    }
483
484
    /**
485
     * {@inheritdoc}
486
     */
487 56
    public function applyPathPrefix($prefix)
488
    {
489 56
        return ltrim(parent::applyPathPrefix($prefix), '/');
490
    }
491
492
    /**
493
     * {@inheritdoc}
494
     */
495 62
    public function setPathPrefix($prefix)
496
    {
497 62
        $prefix = ltrim($prefix, '/');
498
499 62
        return parent::setPathPrefix($prefix);
500
    }
501
502
    /**
503
     * Get the object acl presented as a visibility.
504
     *
505
     * @param string $path
506
     *
507
     * @return string
508
     */
509 12
    protected function getRawVisibility($path)
510
    {
511 12
        $command = $this->s3Client->getCommand(
512 12
            'getObjectAcl',
513
            [
514 12
                'Bucket' => $this->bucket,
515 12
                'Key' => $this->applyPathPrefix($path),
516
            ]
517 12
        );
518
519 12
        $result = $this->s3Client->execute($command);
520 12
        $visibility = AdapterInterface::VISIBILITY_PRIVATE;
521
522 12
        foreach ($result->get('Grants') as $grant) {
523
            if (
524 2
                isset($grant['Grantee']['URI'])
525 2
                && $grant['Grantee']['URI'] === self::PUBLIC_GRANT_URI
526 2
                && $grant['Permission'] === 'READ'
527 2
            ) {
528 2
                $visibility = AdapterInterface::VISIBILITY_PUBLIC;
529 2
                break;
530
            }
531 12
        }
532
533 12
        return $visibility;
534
    }
535
536
    /**
537
     * Upload an object.
538
     *
539
     * @param        $path
540
     * @param        $body
541
     * @param Config $config
542
     *
543
     * @return array
544
     */
545 10
    protected function upload($path, $body, Config $config)
546
    {
547 10
        $key = $this->applyPathPrefix($path);
548 10
        $options = $this->getOptionsFromConfig($config);
549 10
        $acl = isset($options['ACL']) ? $options['ACL'] : 'private';
550
551 10
        if ( ! isset($options['ContentType']) && is_string($body)) {
552 2
            $options['ContentType'] = Util::guessMimeType($path, $body);
553 2
        }
554
555 10
        if ( ! isset($options['ContentLength'])) {
556 10
            $options['ContentLength'] = is_string($body) ? Util::contentSize($body) : Util::getStreamSize($body);
557 10
        }
558
559 10
        $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
560
561 10
        return $this->normalizeResponse($options, $key);
562
    }
563
564
    /**
565
     * Get options from the config.
566
     *
567
     * @param Config $config
568
     *
569
     * @return array
570
     */
571 10
    protected function getOptionsFromConfig(Config $config)
572
    {
573 10
        $options = $this->options;
574
575 10
        if ($visibility = $config->get('visibility')) {
576
            // For local reference
577 8
            $options['visibility'] = $visibility;
578
            // For external reference
579 8
            $options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
580 8
        }
581
582 10
        if ($mimetype = $config->get('mimetype')) {
583
            // For local reference
584 8
            $options['mimetype'] = $mimetype;
585
            // For external reference
586 8
            $options['ContentType'] = $mimetype;
587 8
        }
588
589 10
        foreach (static::$metaOptions as $option) {
590 10
            if ( ! $config->has($option)) {
591 10
                continue;
592
            }
593 8
            $options[$option] = $config->get($option);
594 10
        }
595
596 10
        return $options;
597
    }
598
599
    /**
600
     * Normalize the object result array.
601
     *
602
     * @param array  $response
603
     * @param string $path
604
     *
605
     * @return array
606
     */
607 22
    protected function normalizeResponse(array $response, $path = null)
608
    {
609
        $result = [
610 22
            'path' => $path ?: $this->removePathPrefix(
611
                isset($response['Key']) ? $response['Key'] : $response['Prefix']
612 22
            ),
613 22
        ];
614 22
        $result = array_merge($result, Util::pathinfo($result['path']));
615
616 22
        if (isset($response['LastModified'])) {
617 12
            $result['timestamp'] = strtotime($response['LastModified']);
618 12
        }
619
620 22
        if (substr($result['path'], -1) === '/') {
621 2
            $result['type'] = 'dir';
622 2
            $result['path'] = rtrim($result['path'], '/');
623
624 2
            return $result;
625
        }
626
627 20
        return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
628
    }
629
}
630