Completed
Pull Request — master (#50)
by Raimondas
01:33
created

AwsS3Provider::getFileContent()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 23

Duplication

Lines 16
Ratio 69.57 %

Importance

Changes 0
Metric Value
dl 16
loc 23
rs 9.552
c 0
b 0
f 0
cc 4
nc 4
nop 2
1
<?php
2
3
namespace Publiux\laravelcdn\Providers;
4
5
use Aws\S3\BatchDelete;
6
use Aws\S3\Exception\S3Exception;
7
use Aws\S3\S3Client;
8
use Illuminate\Support\Collection;
9
use Illuminate\Support\Facades\File;
10
use Publiux\laravelcdn\Contracts\CdnHelperInterface;
11
use Publiux\laravelcdn\Providers\Contracts\ProviderInterface;
12
use Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface;
13
use Symfony\Component\Console\Output\ConsoleOutput;
14
use Symfony\Component\Finder\SplFileInfo;
15
16
/**
17
 * Class AwsS3Provider
18
 * Amazon (AWS) S3.
19
 *
20
 *
21
 * @category Driver
22
 *
23
 * @property string  $provider_url
24
 * @property string  $threshold
25
 * @property string  $version
26
 * @property string  $region
27
 * @property string  $credential_key
28
 * @property string  $credential_secret
29
 * @property string  $buckets
30
 * @property string  $acl
31
 * @property string  $cloudfront
32
 * @property string  $cloudfront_url
33
 * @property string $http
34
 * @property array  $compression
35
 *
36
 * @author   Mahmoud Zalt <[email protected]>
37
 */
38
class AwsS3Provider extends Provider implements ProviderInterface
39
{
40
    /**
41
     * All the configurations needed by this class with the
42
     * optional configurations default values.
43
     *
44
     * @var array
45
     */
46
    protected $default = [
47
        'url' => null,
48
        'threshold' => 10,
49
        'compression' => [
50
            'extensions' => [],
51
            'algorithm' => null,
52
            'level' => 9
53
        ],
54
        'providers' => [
55
            'aws' => [
56
                's3' => [
57
                    'version' => null,
58
                    'region' => null,
59
                    'endpoint' => null,
60
                    'buckets' => null,
61
                    'upload_folder' => '',
62
                    'http' => null,
63
                    'acl' => 'public-read',
64
                    'cloudfront' => [
65
                        'use' => false,
66
                        'cdn_url' => null,
67
                    ],
68
                ],
69
            ],
70
        ],
71
    ];
72
73
    /**
74
     * Required configurations (must exist in the config file).
75
     *
76
     * @var array
77
     */
78
    protected $rules = ['version', 'region', 'key', 'secret', 'buckets', 'url'];
79
80
    /**
81
     * this array holds the parsed configuration to be used across the class.
82
     *
83
     * @var Array
84
     */
85
    protected $supplier;
86
87
    /**
88
     * @var Instance of Aws\S3\S3Client
89
     */
90
    protected $s3_client;
91
92
    /**
93
     * @var Instance of Guzzle\Batch\BatchBuilder
94
     */
95
    protected $batch;
96
97
    /**
98
     * @var \Publiux\laravelcdn\Contracts\CdnHelperInterface
99
     */
100
    protected $cdn_helper;
101
102
    /**
103
     * @var \Publiux\laravelcdn\Validators\Contracts\ConfigurationsInterface
104
     */
105
    protected $configurations;
106
107
    /**
108
     * @var \Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface
109
     */
110
    protected $provider_validator;
111
112
    /**
113
     * @param \Symfony\Component\Console\Output\ConsoleOutput $console
114
     * @param \Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface $provider_validator
115
     * @param \Publiux\laravelcdn\Contracts\CdnHelperInterface                    $cdn_helper
116
     */
117
    public function __construct(
118
        ConsoleOutput $console,
119
        ProviderValidatorInterface $provider_validator,
120
        CdnHelperInterface $cdn_helper
121
    ) {
122
        $this->console = $console;
0 ignored issues
show
Documentation Bug introduced by
It seems like $console of type object<Symfony\Component...e\Output\ConsoleOutput> is incompatible with the declared type object<Publiux\laravelcdn\Providers\Instance> of property $console.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
123
        $this->provider_validator = $provider_validator;
124
        $this->cdn_helper = $cdn_helper;
125
    }
126
127
    /**
128
     * Read the configuration and prepare an array with the relevant configurations
129
     * for the (AWS S3) provider. and return itself.
130
     *
131
     * @param $configurations
132
     *
133
     * @return $this
134
     */
135
    public function init($configurations)
136
    {
137
        // merge the received config array with the default configurations array to
138
        // fill missed keys with null or default values.
139
        $this->default = array_replace_recursive($this->default, $configurations);
140
141
        $supplier = [
142
            'provider_url' => $this->default['url'],
143
            'threshold' => $this->default['threshold'],
144
            'version' => $this->default['providers']['aws']['s3']['version'],
145
            'region' => $this->default['providers']['aws']['s3']['region'],
146
            'endpoint' => $this->default['providers']['aws']['s3']['endpoint'],
147
            'buckets' => $this->default['providers']['aws']['s3']['buckets'],
148
            'acl' => $this->default['providers']['aws']['s3']['acl'],
149
            'cloudfront' => $this->default['providers']['aws']['s3']['cloudfront']['use'],
150
            'cloudfront_url' => $this->default['providers']['aws']['s3']['cloudfront']['cdn_url'],
151
            'http' => $this->default['providers']['aws']['s3']['http'],
152
            'upload_folder' => $this->default['providers']['aws']['s3']['upload_folder'],
153
            'compression' => $this->default['compression'],
154
        ];
155
156
        // check if any required configuration is missed
157
        $this->provider_validator->validate($supplier, $this->rules);
158
159
        $this->supplier = $supplier;
160
161
        return $this;
162
    }
163
164
    /**
165
     * Upload assets.
166
     *
167
     * @param $assets
168
     *
169
     * @return bool
170
     */
171
    public function upload($assets)
172
    {
173
        // connect before uploading
174
        $connected = $this->connect();
175
176
        if (!$connected) {
177
            return false;
178
        }
179
180
        // user terminal message
181
        $this->console->writeln('<fg=yellow>Comparing local files and bucket...</fg=yellow>');
182
183
        $assets = $this->getFilesAlreadyOnBucket($assets);
184
185
        // upload each asset file to the CDN
186
        $count = count($assets);
187
        if ($count > 0) {
188
            $this->console->writeln('<fg=yellow>Upload in progress......</fg=yellow>');
189
            $count--;
190
            foreach ($assets as $i => $file) {
191
                try {
192
                    $needsCompression = $this->needCompress($file);
193
                    $this->console->writeln(
194
                        '<fg=magenta>' . str_pad( number_format (100 / $count * $i, 2), 6, ' ',STR_PAD_LEFT) . '% </fg=magenta>' .
195
                        '<fg=cyan>Uploading file path: ' . $file->getRealpath() . '</fg=cyan>' .
196
                        ($needsCompression ? ' <fg=green>Compressed</fg=green>' : '')
197
                    );
198
                    $command = $this->s3_client->getCommand('putObject', [
199
200
                        // the bucket name
201
                        'Bucket' => $this->getBucket(),
202
                        // the path of the file on the server (CDN)
203
                        'Key' => $this->supplier['upload_folder'] . str_replace('\\', '/', $file->getPathName()),
204
                        // the path of the path locally
205
                        'Body' => $this->getFileContent($file, $needsCompression),
206
                        // the permission of the file
207
208
                        'ACL' => $this->acl,
209
                        'CacheControl' => $this->default['providers']['aws']['s3']['cache-control'],
210
                        'Metadata' => $this->default['providers']['aws']['s3']['metadata'],
211
                        'Expires' => $this->default['providers']['aws']['s3']['expires'],
212
                        'ContentType' => File::mimeType($file->getRealPath()),
213
                        'ContentEncoding' => $needsCompression ? $this->compression['algorithm'] : 'identity',
214
                    ]);
215
//                var_dump(get_class($command));exit();
216
217
218
                    $this->s3_client->execute($command);
219
                } catch (S3Exception $e) {
220
                    $this->console->writeln('<fg=red>Upload error: '.$e->getMessage().'</fg=red>');
221
                    return false;
222
                }
223
            }
224
225
            // user terminal message
226
            $this->console->writeln('<fg=green>Upload completed successfully.</fg=green>');
227
        } else {
228
            // user terminal message
229
            $this->console->writeln('<fg=yellow>No new files to upload.</fg=yellow>');
230
        }
231
232
        return true;
233
    }
234
235
    /**
236
     * Create an S3 client instance
237
     * (Note: it will read the credentials form the .env file).
238
     *
239
     * @return bool
240
     */
241
    public function connect()
242
    {
243
        try {
244
            // Instantiate an S3 client
245
            $this->setS3Client(new S3Client([
246
                        'version' => $this->supplier['version'],
247
                        'region' => $this->supplier['region'],
248
                        'endpoint' => $this->supplier['endpoint'],
249
                        'http' => $this->supplier['http']
250
                    ]
251
                )
252
            );
253
        } catch (\Exception $e) {
254
            $this->console->writeln('<fg=red>Connection error: '.$e->getMessage().'</fg=red>');
255
            return false;
256
        }
257
258
        return true;
259
    }
260
261
    /**
262
     * @param $s3_client
263
     */
264
    public function setS3Client($s3_client)
265
    {
266
        $this->s3_client = $s3_client;
267
    }
268
269
    /**
270
     * @param $assets
271
     * @return mixed
272
     */
273
    private function getFilesAlreadyOnBucket($assets)
274
    {
275
        $filesOnAWS = new Collection([]);
276
277
        $files = $this->s3_client->listObjects([
278
            'Bucket' => $this->getBucket(),
279
        ]);
280
281
        if (!$files['Contents']) {
282
            //no files on bucket. lets upload everything found.
283
            return $assets;
284
        }
285
286
        foreach ($files['Contents'] as $file) {
287
            $a = [
288
                'Key' => $file['Key'],
289
                "LastModified" => $file['LastModified']->getTimestamp(),
290
                'Size' => $file['Size']
291
            ];
292
            $filesOnAWS->put($file['Key'], $a);
293
        }
294
295
        $assets->transform(function ($item, $key) use (&$filesOnAWS) {
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
296
            $fileOnAWS = $filesOnAWS->get(str_replace('\\', '/', $item->getPathName()));
297
298
            //select to upload files that are different in size AND last modified time.
299
            if (!($item->getMTime() === $fileOnAWS['LastModified']) && !($item->getSize() === $fileOnAWS['Size'])) {
300
                return $item;
301
            }
302
        });
303
304
        $assets = $assets->reject(function ($item) {
305
            return $item === null;
306
        });
307
308
        return $assets;
309
    }
310
311
    /**
312
     * @return array
313
     */
314
    public function getBucket()
315
    {
316
        // this step is very important, "always assign returned array from
317
        // magical function to a local variable if you need to modify it's
318
        // state or apply any php function on it." because the returned is
319
        // a copy of the original variable. this prevent this error:
320
        // Indirect modification of overloaded property
321
        // Vinelab\Cdn\Providers\AwsS3Provider::$buckets has no effect
322
        $bucket = $this->buckets;
323
324
        return rtrim(key($bucket), '/');
325
    }
326
327
    /**
328
     * Empty bucket.
329
     *
330
     * @return bool
331
     */
332
    public function emptyBucket()
333
    {
334
335
        // connect before uploading
336
        $connected = $this->connect();
337
338
        if (!$connected) {
339
            return false;
340
        }
341
342
        // user terminal message
343
        $this->console->writeln('<fg=yellow>Emptying in progress...</fg=yellow>');
344
345
        try {
346
347
            // Get the contents of the bucket for information purposes
348
            $contents = $this->s3_client->listObjects([
349
                'Bucket' => $this->getBucket(),
350
                'Key' => '',
351
            ]);
352
353
            // Check if the bucket is already empty
354
            if (!$contents['Contents']) {
355
                $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is already empty.</fg=green>');
356
357
                return true;
358
            }
359
360
            // Empty out the bucket
361
            $empty = BatchDelete::fromListObjects($this->s3_client, [
362
                'Bucket' => $this->getBucket(),
363
                'Prefix' => null,
364
            ]);
365
366
            $empty->delete();
367
        } catch (S3Exception $e) {
368
            $this->console->writeln('<fg=red>Deletion error: '.$e->getMessage().'</fg=red>');
369
            return false;
370
        }
371
372
        $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is now empty.</fg=green>');
373
374
        return true;
375
    }
376
377
    /**
378
     * This function will be called from the CdnFacade class when
379
     * someone use this {{ Cdn::asset('') }} facade helper.
380
     *
381
     * @param $path
382
     *
383
     * @return string
384
     */
385
    public function urlGenerator($path)
386
    {
387
        if ($this->getCloudFront() === true) {
388
            $url = $this->cdn_helper->parseUrl($this->getCloudFrontUrl());
389
390
            return $url['scheme'] . '://' . $url['host'] . '/' . $path;
391
        }
392
393
        $url = $this->cdn_helper->parseUrl($this->getUrl());
394
395
        $bucket = $this->getBucket();
396
        $bucket = (!empty($bucket)) ? $bucket.'.' : '';
397
398
        return $url['scheme'] . '://' . $bucket . $url['host'] . '/' . $path;
399
    }
400
401
    /**
402
     * @return string
403
     */
404
    public function getCloudFront()
405
    {
406
        if (!is_bool($cloudfront = $this->cloudfront)) {
407
            return false;
408
        }
409
410
        return $cloudfront;
411
    }
412
413
    /**
414
     * @return string
415
     */
416
    public function getCloudFrontUrl()
417
    {
418
        return rtrim($this->cloudfront_url, '/').'/';
419
    }
420
421
    /**
422
     * @return string
423
     */
424
    public function getUrl()
425
    {
426
        return rtrim($this->provider_url, '/') . '/';
427
    }
428
429
    /**
430
     * @param $attr
431
     *
432
     * @return Mix | null
433
     */
434
    public function __get($attr)
435
    {
436
        return isset($this->supplier[$attr]) ? $this->supplier[$attr] : null;
437
    }
438
439
    /**
440
     * Does file needs compression
441
     *
442
     * @param SplFileInfo $file File info
443
     *
444
     * @return bool
445
     */
446
    private function needCompress(SplFileInfo $file) {
447
        return !empty($this->compression['algorithm']) &&
448
            !empty($this->compression['extensions']) &&
449
            in_array($this->compression['algorithm'], ['gzip', 'deflate']) &&
450
            in_array('.' . $file->getExtension(), $this->compression['extensions']);
451
    }
452
453
    /**
454
     * Read file content and compress
455
     *
456
     * @param SplFileInfo $file             File to read
457
     * @param bool        $needsCompress    Need file to compress
458
     *
459
     * @return resource|string
460
     */
461
    private function getFileContent(SplFileInfo $file, $needsCompress) {
462
        if ($needsCompress) {
463
            switch ($this->compression['algorithm']) {
464 View Code Duplication
                case 'gzip':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
465
                    return gzcompress(
466
                        file_get_contents(
467
                            $file->getRealPath()
468
                        ),
469
                        (int)$this->compression['level'],
470
                        ZLIB_ENCODING_GZIP
471
                    );
472 View Code Duplication
                case 'deflate':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
473
                    return gzcompress(
474
                        file_get_contents(
475
                            $file->getRealPath()
476
                        ),
477
                        (int)$this->compression['level'],
478
                        ZLIB_ENCODING_DEFLATE
479
                    );
480
            }
481
        }
482
        return fopen($file->getRealPath(), 'r');
483
    }
484
}
485