Completed
Push — 2.x ( 6c0af0...25356a )
by Frank
04:46
created

AwsS3V3Adapter::move()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
cc 2
nc 3
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace League\Flysystem\AwsS3V3;
6
7
use Aws\Api\DateTimeResult;
8
use Aws\S3\S3Client;
9
use Aws\S3\S3ClientInterface;
10
use Generator;
11
use League\Flysystem\Config;
12
use League\Flysystem\DirectoryAttributes;
13
use League\Flysystem\FileAttributes;
14
use League\Flysystem\FilesystemAdapter;
15
use League\Flysystem\FilesystemOperationFailed;
16
use League\Flysystem\PathPrefixer;
17
use League\Flysystem\StorageAttributes;
18
use League\Flysystem\UnableToCopyFile;
19
use League\Flysystem\UnableToDeleteFile;
20
use League\Flysystem\UnableToMoveFile;
21
use League\Flysystem\UnableToReadFile;
22
use League\Flysystem\UnableToRetrieveMetadata;
23
use League\Flysystem\UnableToSetVisibility;
24
use League\Flysystem\Visibility;
25
use League\MimeTypeDetection\FinfoMimeTypeDetector;
26
use League\MimeTypeDetection\MimeTypeDetector;
27
use Psr\Http\Message\StreamInterface;
28
use Throwable;
29
30
class AwsS3V3Adapter implements FilesystemAdapter
31
{
32
    /**
33
     * @var array
34
     */
35
    public const AVAILABLE_OPTIONS = [
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
    private const EXTRA_METADATA_FIELDS = [
59
        'Metadata',
60
        'StorageClass',
61
        'ETag',
62
        'VersionId',
63
    ];
64
65
    /**
66
     * @var S3ClientInterface
67
     */
68
    private $client;
69
70
    /**
71
     * @var PathPrefixer
72
     */
73
    private $prefixer;
74
75
    /**
76
     * @var string
77
     */
78
    private $bucket;
79
80
    /**
81
     * @var VisibilityConverter
82
     */
83
    private $visibility;
84
85
    /**
86
     * @var MimeTypeDetector
87
     */
88
    private $mimeTypeDetector;
89
90
    public function __construct(
91
        S3ClientInterface $client,
92
        string $bucket,
93
        string $prefix = '',
94
        VisibilityConverter $visibility = null,
95
        MimeTypeDetector $mimeTypeDetector = null
96
    ) {
97
        $this->client = $client;
98
        $this->prefixer = new PathPrefixer($prefix);
99
        $this->bucket = $bucket;
100
        $this->visibility = $visibility ?: new PortableVisibilityConverter();
101
        $this->mimeTypeDetector = $mimeTypeDetector ?: new FinfoMimeTypeDetector();
102
    }
103
104
    public function fileExists(string $path): bool
105
    {
106
        return $this->client->doesObjectExist($this->bucket, $this->prefixer->prefixPath($path));
107
    }
108
109
    public function write(string $path, string $contents, Config $config): void
110
    {
111
        $this->upload($path, $contents, $config);
112
    }
113
114
    /**
115
     * @param string          $path
116
     * @param string|resource $body
117
     * @param Config          $config
118
     */
119
    private function upload(string $path, $body, Config $config): void
120
    {
121
        $key = $this->prefixer->prefixPath($path);
122
        $acl = $this->determineAcl($config);
123
        $options = $this->createOptionsFromConfig($config);
124
        $shouldDetermineMimetype = $body !== '' && ! array_key_exists('ContentType', $options);
125
126
        if ($shouldDetermineMimetype && $mimeType = $this->mimeTypeDetector->detectMimeType($key, $body)) {
127
            $options['ContentType'] = $mimeType;
128
        }
129
130
        $this->client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
131
    }
132
133
    private function determineAcl(Config $config): string
134
    {
135
        $visibility = (string) $config->get(Config::OPTION_VISIBILITY, Visibility::PRIVATE);
136
137
        return $this->visibility->visibilityToAcl($visibility);
138
    }
139
140
    private function createOptionsFromConfig(Config $config): array
141
    {
142
        $options = [];
143
144
        foreach (static::AVAILABLE_OPTIONS as $option) {
145
            $value = $config->get($option, '__NOT_SET__');
146
147
            if ($value !== '__NOT_SET__') {
148
                $options[$option] = $value;
149
            }
150
        }
151
152
        return $options;
153
    }
154
155
    public function writeStream(string $path, $contents, Config $config): void
156
    {
157
        $this->upload($path, $contents, $config);
158
    }
159
160
    public function read(string $path): string
161
    {
162
        $body = $this->readObject($path);
163
164
        return (string) $body->getContents();
165
    }
166
167
    public function readStream(string $path)
168
    {
169
        /** @var resource $resource */
170
        $resource = $this->readObject($path)->detach();
171
172
        return $resource;
173
    }
174
175
    public function delete(string $path): void
176
    {
177
        $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
178
        $command = $this->client->getCommand('DeleteObject', $arguments);
179
180
        try {
181
            $this->client->execute($command);
182
        } catch (Throwable $exception) {
183
            throw UnableToDeleteFile::atLocation($path, '', $exception);
184
        }
185
    }
186
187
    public function deleteDirectory(string $path): void
188
    {
189
        $prefix = $this->prefixer->prefixPath($path);
190
        $prefix = ltrim(rtrim($prefix, '/') . '/', '/');
191
        $this->client->deleteMatchingObjects($this->bucket, $prefix);
192
    }
193
194
    public function createDirectory(string $path, Config $config): void
195
    {
196
        $config = $config->withDefaults(['visibility' => $this->visibility->defaultForDirectories()]);
197
        $this->upload(rtrim($path, '/') . '/', '', $config);
198
    }
199
200
    public function setVisibility(string $path, $visibility): void
201
    {
202
        $arguments = [
203
            'Bucket' => $this->bucket,
204
            'Key'    => $this->prefixer->prefixPath($path),
205
            'ACL'    => $this->visibility->visibilityToAcl($visibility),
206
        ];
207
        $command = $this->client->getCommand('PutObjectAcl', $arguments);
208
209
        try {
210
            $this->client->execute($command);
211
        } catch (Throwable $exception) {
212
            throw UnableToSetVisibility::atLocation($path, '', $exception);
213
        }
214
    }
215
216
    public function visibility(string $path): FileAttributes
217
    {
218
        $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
219
        $command = $this->client->getCommand('GetObjectAcl', $arguments);
220
221
        try {
222
            $result = $this->client->execute($command);
223
        } catch (Throwable $exception) {
224
            throw UnableToRetrieveMetadata::visibility($path, '', $exception);
225
        }
226
227
        $visibility = $this->visibility->aclToVisibility((array) $result->get('Grants'));
228
229
        return new FileAttributes($path, null, $visibility);
230
    }
231
232
    private function fetchFileMetadata(string $path, string $type): FileAttributes
233
    {
234
        $arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
235
        $command = $this->client->getCommand('HeadObject', $arguments);
236
237
        try {
238
            $result = $this->client->execute($command);
239
        } catch (Throwable $exception) {
240
            throw UnableToRetrieveMetadata::create($path, $type, '', $exception);
241
        }
242
243
        $attributes = $this->mapS3ObjectMetadata($result->toArray(), $path);
244
245
        if ( ! $attributes instanceof FileAttributes) {
246
            throw UnableToRetrieveMetadata::create($path, $type, '');
247
        }
248
249
        return $attributes;
250
    }
251
252
    private function mapS3ObjectMetadata(array $metadata, string $path = null): StorageAttributes
253
    {
254
        if ($path === null) {
255
            $path = $this->prefixer->stripPrefix($metadata['Key'] ?? $metadata['Prefix']);
256
        }
257
258
        if (substr($path, -1) === '/') {
259
            return new DirectoryAttributes(rtrim($path, '/'));
260
        }
261
262
        $mimetype = $metadata['ContentType'] ?? null;
263
        $fileSize = $metadata['ContentLength'] ?? $metadata['Size'] ?? null;
264
        $lastModified = null;
265
        $dateTime = $metadata['LastModified'] ?? null;
266
267
        if ($dateTime instanceof DateTimeResult) {
268
            $lastModified = $dateTime->getTimestamp();
269
        }
270
271
        return new FileAttributes(
272
            $path, (int) $fileSize, null, $lastModified, $mimetype, $this->extractExtraMetadata($metadata)
273
        );
274
    }
275
276
    private function extractExtraMetadata(array $metadata): array
277
    {
278
        $extracted = [];
279
280
        foreach (static::EXTRA_METADATA_FIELDS as $field) {
281
            if (isset($metadata[$field]) && $metadata[$field] !== '') {
282
                $extracted[$field] = $metadata[$field];
283
            }
284
        }
285
286
        return $extracted;
287
    }
288
289
    public function mimeType(string $path): FileAttributes
290
    {
291
        return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_MIME_TYPE);
292
    }
293
294
    public function lastModified(string $path): FileAttributes
295
    {
296
        return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_LAST_MODIFIED);
297
    }
298
299
    public function fileSize(string $path): FileAttributes
300
    {
301
        return $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_FILE_SIZE);
302
    }
303
304
    public function listContents(string $path, bool $deep): iterable
305
    {
306
        $prefix = $this->prefixer->prefixPath($path);
307
        $options = ['Bucket' => $this->bucket, 'Prefix' => trim($prefix, '/') . '/'];
308
309
        if ($deep === false) {
310
            $options['Delimiter'] = '/';
311
        }
312
313
        $listing = $this->retrievePaginatedListing($options);
314
315
        foreach ($listing as $item) {
316
            yield $this->mapS3ObjectMetadata($item);
317
        }
318
    }
319
320
    private function retrievePaginatedListing(array $options): Generator
321
    {
322
        $resultPaginator = $this->client->getPaginator('ListObjects', $options);
323
324
        foreach ($resultPaginator as $result) {
325
            yield from ($result->get('CommonPrefixes') ?: []);
326
            yield from ($result->get('Contents') ?: []);
327
        }
328
    }
329
330
    public function move(string $source, string $destination, Config $config): void
331
    {
332
        try {
333
            $this->copy($source, $destination, $config);
334
            $this->delete($source);
335
        } catch (FilesystemOperationFailed $exception) {
336
            throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);
337
        }
338
    }
339
340
    public function copy(string $source, string $destination, Config $config): void
341
    {
342
        try {
343
            /** @var string $visibility */
344
            $visibility = $this->visibility($source)->visibility();
345
        } catch (Throwable $exception) {
346
            throw UnableToCopyFile::fromLocationTo(
347
                $source,
348
                $destination,
349
                $exception
350
            );
351
        }
352
        $arguments = [
353
            'ACL'        => $this->visibility->visibilityToAcl($visibility),
354
            'Bucket'     => $this->bucket,
355
            'Key'        => $this->prefixer->prefixPath($destination),
356
            'CopySource' => S3Client::encodeKey($this->bucket . '/' . $this->prefixer->prefixPath($source)),
357
        ];
358
        $command = $this->client->getCommand('CopyObject', $arguments);
359
360
        try {
361
            $this->client->execute($command);
362
        } catch (Throwable $exception) {
363
            throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);
364
        }
365
    }
366
367
    private function readObject(string $path): StreamInterface
368
    {
369
        $options = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
370
        $command = $this->client->getCommand('GetObject', $options);
371
372
        try {
373
            return $this->client->execute($command)->get('Body');
374
        } catch (Throwable $exception) {
375
            throw UnableToReadFile::fromLocation($path, '', $exception);
376
        }
377
    }
378
}
379