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

AwsS3Provider::needCompress()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 4
nc 4
nop 1
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
 * @property array  $mimetypes
36
 *
37
 * @author   Mahmoud Zalt <[email protected]>
38
 */
39
class AwsS3Provider extends Provider implements ProviderInterface
40
{
41
    /**
42
     * All the configurations needed by this class with the
43
     * optional configurations default values.
44
     *
45
     * @var array
46
     */
47
    protected $default = [
48
        'url' => null,
49
        'threshold' => 10,
50
        'compression' => [
51
            'extensions' => [],
52
            'algorithm' => null,
53
            'level' => 9
54
        ],
55
        'mimetypes' => [
56
        ],
57
        'providers' => [
58
            'aws' => [
59
                's3' => [
60
                    'version' => null,
61
                    'region' => null,
62
                    'endpoint' => null,
63
                    'buckets' => null,
64
                    'upload_folder' => '',
65
                    'http' => null,
66
                    'acl' => 'public-read',
67
                    'cloudfront' => [
68
                        'use' => false,
69
                        'cdn_url' => null,
70
                    ],
71
                ],
72
            ],
73
        ],
74
    ];
75
76
    /**
77
     * Required configurations (must exist in the config file).
78
     *
79
     * @var array
80
     */
81
    protected $rules = ['version', 'region', 'key', 'secret', 'buckets', 'url', 'mimetypes'];
82
83
    /**
84
     * this array holds the parsed configuration to be used across the class.
85
     *
86
     * @var Array
87
     */
88
    protected $supplier;
89
90
    /**
91
     * @var Instance of Aws\S3\S3Client
92
     */
93
    protected $s3_client;
94
95
    /**
96
     * @var Instance of Guzzle\Batch\BatchBuilder
97
     */
98
    protected $batch;
99
100
    /**
101
     * @var \Publiux\laravelcdn\Contracts\CdnHelperInterface
102
     */
103
    protected $cdn_helper;
104
105
    /**
106
     * @var \Publiux\laravelcdn\Validators\Contracts\ConfigurationsInterface
107
     */
108
    protected $configurations;
109
110
    /**
111
     * @var \Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface
112
     */
113
    protected $provider_validator;
114
115
    /**
116
     * @param \Symfony\Component\Console\Output\ConsoleOutput $console
117
     * @param \Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface $provider_validator
118
     * @param \Publiux\laravelcdn\Contracts\CdnHelperInterface                    $cdn_helper
119
     */
120
    public function __construct(
121
        ConsoleOutput $console,
122
        ProviderValidatorInterface $provider_validator,
123
        CdnHelperInterface $cdn_helper
124
    ) {
125
        $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...
126
        $this->provider_validator = $provider_validator;
127
        $this->cdn_helper = $cdn_helper;
128
    }
129
130
    /**
131
     * Read the configuration and prepare an array with the relevant configurations
132
     * for the (AWS S3) provider. and return itself.
133
     *
134
     * @param $configurations
135
     *
136
     * @return $this
137
     */
138
    public function init($configurations)
139
    {
140
        // merge the received config array with the default configurations array to
141
        // fill missed keys with null or default values.
142
        $this->default = array_replace_recursive($this->default, $configurations);
143
144
        $supplier = [
145
            'provider_url' => $this->default['url'],
146
            'threshold' => $this->default['threshold'],
147
            'version' => $this->default['providers']['aws']['s3']['version'],
148
            'region' => $this->default['providers']['aws']['s3']['region'],
149
            'endpoint' => $this->default['providers']['aws']['s3']['endpoint'],
150
            'buckets' => $this->default['providers']['aws']['s3']['buckets'],
151
            'acl' => $this->default['providers']['aws']['s3']['acl'],
152
            'cloudfront' => $this->default['providers']['aws']['s3']['cloudfront']['use'],
153
            'cloudfront_url' => $this->default['providers']['aws']['s3']['cloudfront']['cdn_url'],
154
            'http' => $this->default['providers']['aws']['s3']['http'],
155
            'upload_folder' => $this->default['providers']['aws']['s3']['upload_folder'],
156
            'compression' => $this->default['compression'],
157
            'mimetypes' => $this->default['mimetypes'],
158
        ];
159
160
        // check if any required configuration is missed
161
        $this->provider_validator->validate($supplier, $this->rules);
162
163
        $this->supplier = $supplier;
164
165
        return $this;
166
    }
167
168
    /**
169
     * Upload assets.
170
     *
171
     * @param $assets
172
     *
173
     * @return bool
174
     */
175
    public function upload($assets)
176
    {
177
        // connect before uploading
178
        $connected = $this->connect();
179
180
        if (!$connected) {
181
            return false;
182
        }
183
184
        // user terminal message
185
        $this->console->writeln('<fg=yellow>Comparing local files and bucket...</fg=yellow>');
186
187
        $assets = $this->getFilesAlreadyOnBucket($assets);
188
189
        // upload each asset file to the CDN
190
        $count = count($assets);
191
        if ($count > 0) {
192
            $this->console->writeln('<fg=yellow>Upload in progress......</fg=yellow>');
193
            foreach ($assets as $i => $file) {
194
                try {
195
                    $needsCompression = $this->needCompress($file);
196
                    $this->console->writeln(
197
                        '<fg=magenta>' . str_pad( number_format (100 / $count * ($i + 1), 2), 6, ' ',STR_PAD_LEFT) . '% </fg=magenta>' .
198
                        '<fg=cyan>Uploading file path: ' . $file->getRealpath() . '</fg=cyan>' .
199
                        ($needsCompression ? ' <fg=green>Compressed</fg=green>' : '')
200
                    );
201
                    $command = $this->s3_client->getCommand('putObject', [
202
203
                        // the bucket name
204
                        'Bucket' => $this->getBucket(),
205
                        // the path of the file on the server (CDN)
206
                        'Key' => $this->supplier['upload_folder'] . str_replace('\\', '/', $file->getPathName()),
207
                        // the path of the path locally
208
                        'Body' => $this->getFileContent($file, $needsCompression),
209
                        // the permission of the file
210
211
                        'ACL' => $this->acl,
212
                        'CacheControl' => $this->default['providers']['aws']['s3']['cache-control'],
213
                        'Metadata' => $this->default['providers']['aws']['s3']['metadata'],
214
                        'Expires' => $this->default['providers']['aws']['s3']['expires'],
215
                        'ContentType' => $this->getMimetype($file),
216
                        'ContentEncoding' => $needsCompression ? $this->compression['algorithm'] : 'identity',
217
                    ]);
218
//                var_dump(get_class($command));exit();
219
220
221
                    $this->s3_client->execute($command);
222
                } catch (S3Exception $e) {
223
                    $this->console->writeln('<fg=red>Upload error: '.$e->getMessage().'</fg=red>');
224
                    return false;
225
                }
226
            }
227
228
            // user terminal message
229
            $this->console->writeln('<fg=green>Upload completed successfully.</fg=green>');
230
        } else {
231
            // user terminal message
232
            $this->console->writeln('<fg=yellow>No new files to upload.</fg=yellow>');
233
        }
234
235
        return true;
236
    }
237
238
    /**
239
     * Create an S3 client instance
240
     * (Note: it will read the credentials form the .env file).
241
     *
242
     * @return bool
243
     */
244
    public function connect()
245
    {
246
        try {
247
            // Instantiate an S3 client
248
            $this->setS3Client(new S3Client([
249
                        'version' => $this->supplier['version'],
250
                        'region' => $this->supplier['region'],
251
                        'endpoint' => $this->supplier['endpoint'],
252
                        'http' => $this->supplier['http']
253
                    ]
254
                )
255
            );
256
        } catch (\Exception $e) {
257
            $this->console->writeln('<fg=red>Connection error: '.$e->getMessage().'</fg=red>');
258
            return false;
259
        }
260
261
        return true;
262
    }
263
264
    /**
265
     * @param $s3_client
266
     */
267
    public function setS3Client($s3_client)
268
    {
269
        $this->s3_client = $s3_client;
270
    }
271
272
    /**
273
     * @param $assets
274
     * @return mixed
275
     */
276
    private function getFilesAlreadyOnBucket($assets)
277
    {
278
        $filesOnAWS = new Collection([]);
279
280
        $files = $this->s3_client->listObjects([
281
            'Bucket' => $this->getBucket(),
282
        ]);
283
284
        if (!$files['Contents']) {
285
            //no files on bucket. lets upload everything found.
286
            return $assets;
287
        }
288
289
        foreach ($files['Contents'] as $file) {
290
            $a = [
291
                'Key' => $file['Key'],
292
                "LastModified" => $file['LastModified']->getTimestamp(),
293
                'Size' => $file['Size']
294
            ];
295
            $filesOnAWS->put($file['Key'], $a);
296
        }
297
298
        $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...
299
            $fileOnAWS = $filesOnAWS->get(str_replace('\\', '/', $item->getPathName()));
300
301
            //select to upload files that are different in size AND last modified time.
302
            if (!($item->getMTime() === $fileOnAWS['LastModified']) && !($item->getSize() === $fileOnAWS['Size'])) {
303
                return $item;
304
            }
305
        });
306
307
        $assets = $assets->reject(function ($item) {
308
            return $item === null;
309
        });
310
311
        return $assets;
312
    }
313
314
    /**
315
     * @return array
316
     */
317
    public function getBucket()
318
    {
319
        // this step is very important, "always assign returned array from
320
        // magical function to a local variable if you need to modify it's
321
        // state or apply any php function on it." because the returned is
322
        // a copy of the original variable. this prevent this error:
323
        // Indirect modification of overloaded property
324
        // Vinelab\Cdn\Providers\AwsS3Provider::$buckets has no effect
325
        $bucket = $this->buckets;
326
327
        return rtrim(key($bucket), '/');
328
    }
329
330
    /**
331
     * Empty bucket.
332
     *
333
     * @return bool
334
     */
335
    public function emptyBucket()
336
    {
337
338
        // connect before uploading
339
        $connected = $this->connect();
340
341
        if (!$connected) {
342
            return false;
343
        }
344
345
        // user terminal message
346
        $this->console->writeln('<fg=yellow>Emptying in progress...</fg=yellow>');
347
348
        try {
349
350
            // Get the contents of the bucket for information purposes
351
            $contents = $this->s3_client->listObjects([
352
                'Bucket' => $this->getBucket(),
353
                'Key' => '',
354
            ]);
355
356
            // Check if the bucket is already empty
357
            if (!$contents['Contents']) {
358
                $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is already empty.</fg=green>');
359
360
                return true;
361
            }
362
363
            // Empty out the bucket
364
            $empty = BatchDelete::fromListObjects($this->s3_client, [
365
                'Bucket' => $this->getBucket(),
366
                'Prefix' => null,
367
            ]);
368
369
            $empty->delete();
370
        } catch (S3Exception $e) {
371
            $this->console->writeln('<fg=red>Deletion error: '.$e->getMessage().'</fg=red>');
372
            return false;
373
        }
374
375
        $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is now empty.</fg=green>');
376
377
        return true;
378
    }
379
380
    /**
381
     * This function will be called from the CdnFacade class when
382
     * someone use this {{ Cdn::asset('') }} facade helper.
383
     *
384
     * @param $path
385
     *
386
     * @return string
387
     */
388
    public function urlGenerator($path)
389
    {
390
        if ($this->getCloudFront() === true) {
391
            $url = $this->cdn_helper->parseUrl($this->getCloudFrontUrl());
392
393
            return $url['scheme'] . '://' . $url['host'] . '/' . $path;
394
        }
395
396
        $url = $this->cdn_helper->parseUrl($this->getUrl());
397
398
        $bucket = $this->getBucket();
399
        $bucket = (!empty($bucket)) ? $bucket.'.' : '';
400
401
        return $url['scheme'] . '://' . $bucket . $url['host'] . '/' . $path;
402
    }
403
404
    /**
405
     * @return string
406
     */
407
    public function getCloudFront()
408
    {
409
        if (!is_bool($cloudfront = $this->cloudfront)) {
410
            return false;
411
        }
412
413
        return $cloudfront;
414
    }
415
416
    /**
417
     * @return string
418
     */
419
    public function getCloudFrontUrl()
420
    {
421
        return rtrim($this->cloudfront_url, '/').'/';
422
    }
423
424
    /**
425
     * @return string
426
     */
427
    public function getUrl()
428
    {
429
        return rtrim($this->provider_url, '/') . '/';
430
    }
431
432
    /**
433
     * @param $attr
434
     *
435
     * @return Mix | null
436
     */
437
    public function __get($attr)
438
    {
439
        return isset($this->supplier[$attr]) ? $this->supplier[$attr] : null;
440
    }
441
442
    /**
443
     * Does file needs compression
444
     *
445
     * @param SplFileInfo $file File info
446
     *
447
     * @return bool
448
     */
449
    private function needCompress(SplFileInfo $file) {
450
        return !empty($this->compression['algorithm']) &&
451
            !empty($this->compression['extensions']) &&
452
            in_array($this->compression['algorithm'], ['gzip', 'deflate']) &&
453
            in_array('.' . $file->getExtension(), $this->compression['extensions']);
454
    }
455
456
    /**
457
     * Read file content and compress
458
     *
459
     * @param SplFileInfo $file             File to read
460
     * @param bool        $needsCompress    Need file to compress
461
     *
462
     * @return resource|string
463
     */
464
    private function getFileContent(SplFileInfo $file, $needsCompress) {
465
        if ($needsCompress) {
466
            switch ($this->compression['algorithm']) {
467 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...
468
                    return gzcompress(
469
                        file_get_contents(
470
                            $file->getRealPath()
471
                        ),
472
                        (int)$this->compression['level'],
473
                        ZLIB_ENCODING_GZIP
474
                    );
475 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...
476
                    return gzcompress(
477
                        file_get_contents(
478
                            $file->getRealPath()
479
                        ),
480
                        (int)$this->compression['level'],
481
                        ZLIB_ENCODING_DEFLATE
482
                    );
483
            }
484
        }
485
        return fopen($file->getRealPath(), 'r');
486
    }
487
488
    /**
489
     * Get mimetype from config or from system
490
     *
491
     * @param SplFileInfo $file File info to get mimetype
492
     *
493
     * @return false|string
494
     */
495
    protected function getMimetype(SplFileInfo $file) {
496
        $ext = '.' . $file->getExtension();
497
        if (isset($this->mimetypes[$ext])) {
498
            return $this->mimetypes[$ext];
499
        }
500
        return File::mimeType($file->getRealPath());
501
    }
502
503
}
504