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

AwsS3Provider::emptyBucket()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

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