Completed
Pull Request — master (#51)
by Raimondas
01:28
created

AwsS3Provider::calculateEtag()   C

Complexity

Conditions 17
Paths 80

Size

Total Lines 61

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 61
rs 5.2166
c 0
b 0
f 0
cc 17
nc 80
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Publiux\laravelcdn\Contracts\CdnHelperInterface;
10
use Publiux\laravelcdn\Providers\Contracts\ProviderInterface;
11
use Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface;
12
use Symfony\Component\Console\Output\ConsoleOutput;
13
14
/**
15
 * Class AwsS3Provider
16
 * Amazon (AWS) S3.
17
 *
18
 *
19
 * @category Driver
20
 *
21
 * @property string  $provider_url
22
 * @property string  $threshold
23
 * @property string  $version
24
 * @property string  $region
25
 * @property string  $credential_key
26
 * @property string  $credential_secret
27
 * @property string  $buckets
28
 * @property string  $acl
29
 * @property string  $cloudfront
30
 * @property string  $cloudfront_url
31
 * @property string $http
32
 *
33
 * @author   Mahmoud Zalt <[email protected]>
34
 */
35
class AwsS3Provider extends Provider implements ProviderInterface
36
{
37
    /**
38
     * All the configurations needed by this class with the
39
     * optional configurations default values.
40
     *
41
     * @var array
42
     */
43
    protected $default = [
44
        'url' => null,
45
        'threshold' => 10,
46
        'providers' => [
47
            'aws' => [
48
                's3' => [
49
                    'version' => null,
50
                    'region' => null,
51
                    'endpoint' => null,
52
                    'buckets' => null,
53
                    'upload_folder' => '',
54
                    'http' => null,
55
                    'acl' => 'public-read',
56
                    'cloudfront' => [
57
                        'use' => false,
58
                        'cdn_url' => null,
59
                    ],
60
                ],
61
            ],
62
        ],
63
    ];
64
65
    /**
66
     * Required configurations (must exist in the config file).
67
     *
68
     * @var array
69
     */
70
    protected $rules = ['version', 'region', 'key', 'secret', 'buckets', 'url'];
71
72
    /**
73
     * this array holds the parsed configuration to be used across the class.
74
     *
75
     * @var Array
76
     */
77
    protected $supplier;
78
79
    /**
80
     * @var Instance of Aws\S3\S3Client
81
     */
82
    protected $s3_client;
83
84
    /**
85
     * @var Instance of Guzzle\Batch\BatchBuilder
86
     */
87
    protected $batch;
88
89
    /**
90
     * @var \Publiux\laravelcdn\Contracts\CdnHelperInterface
91
     */
92
    protected $cdn_helper;
93
94
    /**
95
     * @var \Publiux\laravelcdn\Validators\Contracts\ConfigurationsInterface
96
     */
97
    protected $configurations;
98
99
    /**
100
     * @var \Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface
101
     */
102
    protected $provider_validator;
103
104
    /**
105
     * @param \Symfony\Component\Console\Output\ConsoleOutput $console
106
     * @param \Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface $provider_validator
107
     * @param \Publiux\laravelcdn\Contracts\CdnHelperInterface                    $cdn_helper
108
     */
109
    public function __construct(
110
        ConsoleOutput $console,
111
        ProviderValidatorInterface $provider_validator,
112
        CdnHelperInterface $cdn_helper
113
    ) {
114
        $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...
115
        $this->provider_validator = $provider_validator;
116
        $this->cdn_helper = $cdn_helper;
117
    }
118
119
    /**
120
     * Read the configuration and prepare an array with the relevant configurations
121
     * for the (AWS S3) provider. and return itself.
122
     *
123
     * @param $configurations
124
     *
125
     * @return $this
126
     */
127
    public function init($configurations)
128
    {
129
        // merge the received config array with the default configurations array to
130
        // fill missed keys with null or default values.
131
        $this->default = array_replace_recursive($this->default, $configurations);
132
133
        $supplier = [
134
            'provider_url' => $this->default['url'],
135
            'threshold' => $this->default['threshold'],
136
            'version' => $this->default['providers']['aws']['s3']['version'],
137
            'region' => $this->default['providers']['aws']['s3']['region'],
138
            'endpoint' => $this->default['providers']['aws']['s3']['endpoint'],
139
            'buckets' => $this->default['providers']['aws']['s3']['buckets'],
140
            'acl' => $this->default['providers']['aws']['s3']['acl'],
141
            'cloudfront' => $this->default['providers']['aws']['s3']['cloudfront']['use'],
142
            'cloudfront_url' => $this->default['providers']['aws']['s3']['cloudfront']['cdn_url'],
143
            'http' => $this->default['providers']['aws']['s3']['http'],
144
            'upload_folder' => $this->default['providers']['aws']['s3']['upload_folder']
145
        ];
146
147
        // check if any required configuration is missed
148
        $this->provider_validator->validate($supplier, $this->rules);
149
150
        $this->supplier = $supplier;
151
152
        return $this;
153
    }
154
155
    /**
156
     * Upload assets.
157
     *
158
     * @param $assets
159
     *
160
     * @return bool
161
     */
162
    public function upload($assets)
163
    {
164
        // connect before uploading
165
        $connected = $this->connect();
166
167
        if (!$connected) {
168
            return false;
169
        }
170
171
        // user terminal message
172
        $this->console->writeln('<fg=yellow>Comparing local files and bucket...</fg=yellow>');
173
174
        $assets = $this->getFilesToUpload($assets);
175
176
        // upload each asset file to the CDN
177
        if (count($assets) > 0) {
178
            $this->console->writeln('<fg=yellow>Upload in progress......</fg=yellow>');
179
            foreach ($assets as $file) {
180
                try {
181
                    $this->console->writeln('<fg=cyan>'.'Uploading file path: '.$file->getRealpath().'</fg=cyan>');
182
                    $command = $this->s3_client->getCommand('putObject', [
183
184
                        // the bucket name
185
                        'Bucket' => $this->getBucket(),
186
                        // the path of the file on the server (CDN)
187
                        'Key' => $this->supplier['upload_folder'] . str_replace('\\', '/', $file->getPathName()),
188
                        // the path of the path locally
189
                        'Body' => fopen($file->getRealPath(), 'r'),
190
                        // the permission of the file
191
192
                        'ACL' => $this->acl,
193
                        'CacheControl' => $this->default['providers']['aws']['s3']['cache-control'],
194
                        'Metadata' => $this->default['providers']['aws']['s3']['metadata'],
195
                        'Expires' => $this->default['providers']['aws']['s3']['expires'],
196
                    ]);
197
//                var_dump(get_class($command));exit();
198
199
200
                    $this->s3_client->execute($command);
201
                } catch (S3Exception $e) {
202
                    $this->console->writeln('<fg=red>Upload error: '.$e->getMessage().'</fg=red>');
203
                    return false;
204
                }
205
            }
206
207
            // user terminal message
208
            $this->console->writeln('<fg=green>Upload completed successfully.</fg=green>');
209
        } else {
210
            // user terminal message
211
            $this->console->writeln('<fg=yellow>No new files to upload.</fg=yellow>');
212
        }
213
214
        return true;
215
    }
216
217
    /**
218
     * Create an S3 client instance
219
     * (Note: it will read the credentials form the .env file).
220
     *
221
     * @return bool
222
     */
223
    public function connect()
224
    {
225
        try {
226
            // Instantiate an S3 client
227
            $this->setS3Client(new S3Client([
228
                        'version' => $this->supplier['version'],
229
                        'region' => $this->supplier['region'],
230
                        'endpoint' => $this->supplier['endpoint'],
231
                        'http' => $this->supplier['http']
232
                    ]
233
                )
234
            );
235
        } catch (\Exception $e) {
236
            $this->console->writeln('<fg=red>Connection error: '.$e->getMessage().'</fg=red>');
237
            return false;
238
        }
239
240
        return true;
241
    }
242
243
    /**
244
     * @param $s3_client
245
     */
246
    public function setS3Client($s3_client)
247
    {
248
        $this->s3_client = $s3_client;
249
    }
250
251
    /**
252
     * Get files to upload
253
     *
254
     * @param $assets
255
     * @return mixed
256
     */
257
    private function getFilesToUpload($assets)
258
    {
259
        $assets_dict = new Collection();
260
        foreach ($assets as $item) {
261
            $assets_dict->put(str_replace('\\', '/', $item->getPathName()), $item);
262
        }
263
264
        try {
265
            $results = $this->s3_client->getPaginator('ListObjects', [
266
                'Bucket' => $this->getBucket(),
267
            ]);
268
269
            foreach ($results as $result) {
270
                foreach ($result['Contents'] as $file) {
271
                    $key = $file['Key'];
272
                    if (!$assets_dict->has($key)) {
273
                        continue;
274
                    }
275
                    $item = $assets_dict->get($key);
276
277
                    if (
278
                        !$this->calculateEtag(
279
                            $item->getPathName(),
280
                            -8,
281
                            trim($file['ETag'], '"')
282
                        )
283
                    ) {
284
                        continue;
285
                    }
286
287
                    $assets_dict->forget($key);
288
                }
289
            }
290
291
            return new Collection($assets_dict->values());
292
        } catch (S3Exception $e) {
293
            return $assets;
294
        }
295
    }
296
297
    /**
298
     * @return array
299
     */
300
    public function getBucket()
301
    {
302
        // this step is very important, "always assign returned array from
303
        // magical function to a local variable if you need to modify it's
304
        // state or apply any php function on it." because the returned is
305
        // a copy of the original variable. this prevent this error:
306
        // Indirect modification of overloaded property
307
        // Vinelab\Cdn\Providers\AwsS3Provider::$buckets has no effect
308
        $bucket = $this->buckets;
309
310
        return rtrim(key($bucket), '/');
311
    }
312
313
    /**
314
     * Empty bucket.
315
     *
316
     * @return bool
317
     */
318
    public function emptyBucket()
319
    {
320
321
        // connect before uploading
322
        $connected = $this->connect();
323
324
        if (!$connected) {
325
            return false;
326
        }
327
328
        // user terminal message
329
        $this->console->writeln('<fg=yellow>Emptying in progress...</fg=yellow>');
330
331
        try {
332
333
            // Get the contents of the bucket for information purposes
334
            $contents = $this->s3_client->listObjects([
335
                'Bucket' => $this->getBucket(),
336
                'Key' => '',
337
            ]);
338
339
            // Check if the bucket is already empty
340
            if (!$contents['Contents']) {
341
                $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is already empty.</fg=green>');
342
343
                return true;
344
            }
345
346
            // Empty out the bucket
347
            $empty = BatchDelete::fromListObjects($this->s3_client, [
348
                'Bucket' => $this->getBucket(),
349
                'Prefix' => null,
350
            ]);
351
352
            $empty->delete();
353
        } catch (S3Exception $e) {
354
            $this->console->writeln('<fg=red>Deletion error: '.$e->getMessage().'</fg=red>');
355
            return false;
356
        }
357
358
        $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is now empty.</fg=green>');
359
360
        return true;
361
    }
362
363
    /**
364
     * This function will be called from the CdnFacade class when
365
     * someone use this {{ Cdn::asset('') }} facade helper.
366
     *
367
     * @param $path
368
     *
369
     * @return string
370
     */
371
    public function urlGenerator($path)
372
    {
373
        if ($this->getCloudFront() === true) {
374
            $url = $this->cdn_helper->parseUrl($this->getCloudFrontUrl());
375
376
            return $url['scheme'] . '://' . $url['host'] . '/' . $path;
377
        }
378
379
        $url = $this->cdn_helper->parseUrl($this->getUrl());
380
381
        $bucket = $this->getBucket();
382
        $bucket = (!empty($bucket)) ? $bucket.'.' : '';
383
384
        return $url['scheme'] . '://' . $bucket . $url['host'] . '/' . $path;
385
    }
386
387
    /**
388
     * @return string
389
     */
390
    public function getCloudFront()
391
    {
392
        if (!is_bool($cloudfront = $this->cloudfront)) {
393
            return false;
394
        }
395
396
        return $cloudfront;
397
    }
398
399
    /**
400
     * @return string
401
     */
402
    public function getCloudFrontUrl()
403
    {
404
        return rtrim($this->cloudfront_url, '/').'/';
405
    }
406
407
    /**
408
     * @return string
409
     */
410
    public function getUrl()
411
    {
412
        return rtrim($this->provider_url, '/') . '/';
413
    }
414
    
415
    /**
416
     * Calculate Amazon AWS ETag used on the S3 service
417
     *
418
     * @see https://stackoverflow.com/a/36072294/1762839
419
     * @author TheStoryCoder (https://stackoverflow.com/users/2404541/thestorycoder)
420
     *
421
     * @param string $filename path to file to check
422
     * @param int $chunksize chunk size in Megabytes
423
     * @param bool|string $expected verify calculated etag against this specified
424
     *                              etag and return true or false instead if you make
425
     *                              chunksize negative (eg. -8 instead of 8) the function
426
     *                              will guess the chunksize by checking all possible sizes
427
     *                              given the number of parts mentioned in $expected
428
     *
429
     * @return bool|string          ETag (string) or boolean true|false if $expected is set
430
     */
431
    protected function calculateEtag($filename, $chunksize, $expected = false) {
432
        if ($chunksize < 0) {
433
            $do_guess = true;
434
            $chunksize = 0 - $chunksize;
435
        } else {
436
            $do_guess = false;
437
        }
438
439
        $chunkbytes = $chunksize*1024*1024;
440
        $filesize = filesize($filename);
441
        if ($filesize < $chunkbytes && (!$expected || !preg_match("/^\\w{32}-\\w+$/", $expected))) {
442
            $return = md5_file($filename);
443
            if ($expected) {
444
                $expected = strtolower($expected);
445
                return ($expected === $return ? true : false);
446
            } else {
447
                return $return;
448
            }
449
        } else {
450
            $md5s = array();
451
            $handle = fopen($filename, 'rb');
452
            if ($handle === false) {
453
                return false;
454
            }
455
            while (!feof($handle)) {
456
                $buffer = fread($handle, $chunkbytes);
457
                $md5s[] = md5($buffer);
458
                unset($buffer);
459
            }
460
            fclose($handle);
461
462
            $concat = '';
463
            foreach ($md5s as $indx => $md5) {
464
                $concat .= hex2bin($md5);
465
            }
466
            $return = md5($concat) .'-'. count($md5s);
467
            if ($expected) {
468
                $expected = strtolower($expected);
469
                $matches = ($expected === $return ? true : false);
470
                if ($matches || $do_guess == false || strlen($expected) == 32) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
471
                    return $matches;
472
                } else {
473
                    // Guess the chunk size
474
                    preg_match("/-(\\d+)$/", $expected, $match);
475
                    $parts = $match[1];
476
                    $min_chunk = ceil($filesize / $parts /1024/1024);
477
                    $max_chunk =  floor($filesize / ($parts-1) /1024/1024);
478
                    $found_match = false;
479
                    for ($i = $min_chunk; $i <= $max_chunk; $i++) {
480
                        if ($this->calculateEtag($filename, $i) === $expected) {
481
                            $found_match = true;
482
                            break;
483
                        }
484
                    }
485
                    return $found_match;
486
                }
487
            } else {
488
                return $return;
489
            }
490
        }
491
    }
492
493
494
    /**
495
     * @param $attr
496
     *
497
     * @return Mix | null
498
     */
499
    public function __get($attr)
500
    {
501
        return isset($this->supplier[$attr]) ? $this->supplier[$attr] : null;
502
    }
503
}
504