Completed
Pull Request — master (#50)
by Raimondas
01:43
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
 * @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
            $o = 0;
194
            foreach ($assets as $file) {
195
                try {
196
                    $needsCompression = $this->needCompress($file);
197
                    $this->console->writeln(
198
                        '<fg=magenta>' . str_pad( number_format (100 / $count * ++$o, 2), 6, ' ',STR_PAD_LEFT) . '% </fg=magenta>' .
199
                        '<fg=cyan>Uploading file path: ' . $file->getRealpath() . '</fg=cyan>' .
200
                        ($needsCompression ? PHP_EOL. "        <fg=green>Compressed</fg=green>" : '')
201
                    );
202
                    $command = $this->s3_client->getCommand('putObject', [
203
204
                        // the bucket name
205
                        'Bucket' => $this->getBucket(),
206
                        // the path of the file on the server (CDN)
207
                        'Key' => $this->supplier['upload_folder'] . str_replace('\\', '/', $file->getPathName()),
208
                        // the path of the path locally
209
                        'Body' => $this->getFileContent($file, $needsCompression),
210
                        // the permission of the file
211
212
                        'ACL' => $this->acl,
213
                        'CacheControl' => $this->default['providers']['aws']['s3']['cache-control'],
214
                        'Metadata' => $this->default['providers']['aws']['s3']['metadata'],
215
                        'Expires' => $this->default['providers']['aws']['s3']['expires'],
216
                        'ContentType' => $this->getMimetype($file),
217
                        'ContentEncoding' => $needsCompression ? $this->compression['algorithm'] : 'identity',
218
                    ]);
219
//                var_dump(get_class($command));exit();
220
221
222
                    $this->s3_client->execute($command);
223
                } catch (S3Exception $e) {
224
                    $this->console->writeln('<fg=red>Upload error: '.$e->getMessage().'</fg=red>');
225
                    return false;
226
                }
227
            }
228
229
            // user terminal message
230
            $this->console->writeln('<fg=green>Upload completed successfully.</fg=green>');
231
        } else {
232
            // user terminal message
233
            $this->console->writeln('<fg=yellow>No new files to upload.</fg=yellow>');
234
        }
235
236
        return true;
237
    }
238
239
    /**
240
     * Create an S3 client instance
241
     * (Note: it will read the credentials form the .env file).
242
     *
243
     * @return bool
244
     */
245
    public function connect()
246
    {
247
        try {
248
            // Instantiate an S3 client
249
            $this->setS3Client(new S3Client([
250
                        'version' => $this->supplier['version'],
251
                        'region' => $this->supplier['region'],
252
                        'endpoint' => $this->supplier['endpoint'],
253
                        'http' => $this->supplier['http']
254
                    ]
255
                )
256
            );
257
        } catch (\Exception $e) {
258
            $this->console->writeln('<fg=red>Connection error: '.$e->getMessage().'</fg=red>');
259
            return false;
260
        }
261
262
        return true;
263
    }
264
265
    /**
266
     * @param $s3_client
267
     */
268
    public function setS3Client($s3_client)
269
    {
270
        $this->s3_client = $s3_client;
271
    }
272
273
    /**
274
     * @param $assets
275
     * @return mixed
276
     */
277
    private function getFilesAlreadyOnBucket($assets)
278
    {
279
        $filesOnAWS = new Collection([]);
280
281
        $files = $this->s3_client->listObjects([
282
            'Bucket' => $this->getBucket(),
283
        ]);
284
285
        if (!$files['Contents']) {
286
            //no files on bucket. lets upload everything found.
287
            return $assets;
288
        }
289
290
        foreach ($files['Contents'] as $file) {
291
            $a = [
292
                'Key' => $file['Key'],
293
                "LastModified" => $file['LastModified']->getTimestamp(),
294
                'Size' => $file['Size']
295
            ];
296
            $filesOnAWS->put($file['Key'], $a);
297
        }
298
299
        $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...
300
            $fileOnAWS = $filesOnAWS->get(str_replace('\\', '/', $item->getPathName()));
301
302
            //select to upload files that are different in size AND last modified time.
303
            if (!($item->getMTime() === $fileOnAWS['LastModified']) && !($item->getSize() === $fileOnAWS['Size'])) {
304
                return $item;
305
            }
306
        });
307
308
        $assets = $assets->reject(function ($item) {
309
            return $item === null;
310
        });
311
312
        return $assets;
313
    }
314
315
    /**
316
     * @return array
317
     */
318
    public function getBucket()
319
    {
320
        // this step is very important, "always assign returned array from
321
        // magical function to a local variable if you need to modify it's
322
        // state or apply any php function on it." because the returned is
323
        // a copy of the original variable. this prevent this error:
324
        // Indirect modification of overloaded property
325
        // Vinelab\Cdn\Providers\AwsS3Provider::$buckets has no effect
326
        $bucket = $this->buckets;
327
328
        return rtrim(key($bucket), '/');
329
    }
330
331
    /**
332
     * Empty bucket.
333
     *
334
     * @return bool
335
     */
336
    public function emptyBucket()
337
    {
338
339
        // connect before uploading
340
        $connected = $this->connect();
341
342
        if (!$connected) {
343
            return false;
344
        }
345
346
        // user terminal message
347
        $this->console->writeln('<fg=yellow>Emptying in progress...</fg=yellow>');
348
349
        try {
350
351
            // Get the contents of the bucket for information purposes
352
            $contents = $this->s3_client->listObjects([
353
                'Bucket' => $this->getBucket(),
354
                'Key' => '',
355
            ]);
356
357
            // Check if the bucket is already empty
358
            if (!$contents['Contents']) {
359
                $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is already empty.</fg=green>');
360
361
                return true;
362
            }
363
364
            // Empty out the bucket
365
            $empty = BatchDelete::fromListObjects($this->s3_client, [
366
                'Bucket' => $this->getBucket(),
367
                'Prefix' => null,
368
            ]);
369
370
            $empty->delete();
371
        } catch (S3Exception $e) {
372
            $this->console->writeln('<fg=red>Deletion error: '.$e->getMessage().'</fg=red>');
373
            return false;
374
        }
375
376
        $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is now empty.</fg=green>');
377
378
        return true;
379
    }
380
381
    /**
382
     * This function will be called from the CdnFacade class when
383
     * someone use this {{ Cdn::asset('') }} facade helper.
384
     *
385
     * @param $path
386
     *
387
     * @return string
388
     */
389
    public function urlGenerator($path)
390
    {
391
        if ($this->getCloudFront() === true) {
392
            $url = $this->cdn_helper->parseUrl($this->getCloudFrontUrl());
393
394
            return $url['scheme'] . '://' . $url['host'] . '/' . $path;
395
        }
396
397
        $url = $this->cdn_helper->parseUrl($this->getUrl());
398
399
        $bucket = $this->getBucket();
400
        $bucket = (!empty($bucket)) ? $bucket.'.' : '';
401
402
        return $url['scheme'] . '://' . $bucket . $url['host'] . '/' . $path;
403
    }
404
405
    /**
406
     * @return string
407
     */
408
    public function getCloudFront()
409
    {
410
        if (!is_bool($cloudfront = $this->cloudfront)) {
411
            return false;
412
        }
413
414
        return $cloudfront;
415
    }
416
417
    /**
418
     * @return string
419
     */
420
    public function getCloudFrontUrl()
421
    {
422
        return rtrim($this->cloudfront_url, '/').'/';
423
    }
424
425
    /**
426
     * @return string
427
     */
428
    public function getUrl()
429
    {
430
        return rtrim($this->provider_url, '/') . '/';
431
    }
432
433
    /**
434
     * @param $attr
435
     *
436
     * @return Mix | null
437
     */
438
    public function __get($attr)
439
    {
440
        return isset($this->supplier[$attr]) ? $this->supplier[$attr] : null;
441
    }
442
443
    /**
444
     * Does file needs compression
445
     *
446
     * @param SplFileInfo $file File info
447
     *
448
     * @return bool
449
     */
450
    private function needCompress(SplFileInfo $file) {
451
        return !empty($this->compression['algorithm']) &&
452
            !empty($this->compression['extensions']) &&
453
            in_array($this->compression['algorithm'], ['gzip', 'deflate']) &&
454
            in_array('.' . $file->getExtension(), $this->compression['extensions']);
455
    }
456
457
    /**
458
     * Read file content and compress
459
     *
460
     * @param SplFileInfo $file             File to read
461
     * @param bool        $needsCompress    Need file to compress
462
     *
463
     * @return resource|string
464
     */
465
    private function getFileContent(SplFileInfo $file, $needsCompress) {
466
        if ($needsCompress) {
467
            switch ($this->compression['algorithm']) {
468 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...
469
                    return gzcompress(
470
                        file_get_contents(
471
                            $file->getRealPath()
472
                        ),
473
                        (int)$this->compression['level'],
474
                        ZLIB_ENCODING_GZIP
475
                    );
476 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...
477
                    return gzcompress(
478
                        file_get_contents(
479
                            $file->getRealPath()
480
                        ),
481
                        (int)$this->compression['level'],
482
                        ZLIB_ENCODING_DEFLATE
483
                    );
484
            }
485
        }
486
        return fopen($file->getRealPath(), 'r');
487
    }
488
489
    /**
490
     * Get mimetype from config or from system
491
     *
492
     * @param SplFileInfo $file File info to get mimetype
493
     *
494
     * @return false|string
495
     */
496
    protected function getMimetype(SplFileInfo $file) {
497
        $ext = '.' . $file->getExtension();
498
        if (isset($this->mimetypes[$ext])) {
499
            return $this->mimetypes[$ext];
500
        }
501
        return File::mimeType($file->getRealPath());
502
    }
503
504
}
505