Completed
Push — master ( ea7a05...07212d )
by Raul
02:50 queued 15s
created

AwsS3Provider::getFilesAlreadyOnBucket()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 1
Metric Value
c 2
b 1
f 1
dl 0
loc 33
rs 8.439
cc 5
eloc 16
nc 3
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 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
 *
32
 * @author   Mahmoud Zalt <[email protected]>
33
 * @author   Raul Ruiz <[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
                    'buckets'    => null,
52
                    'acl'        => 'public-read',
53
                    'cloudfront' => [
54
                        'use'     => false,
55
                        'cdn_url' => null,
56
                    ],
57
                ],
58
            ],
59
        ],
60
    ];
61
62
    /**
63
     * Required configurations (must exist in the config file).
64
     *
65
     * @var array
66
     */
67
    protected $rules = ['version', 'region', 'key', 'secret', 'buckets', 'url'];
68
69
    /**
70
     * this array holds the parsed configuration to be used across the class.
71
     *
72
     * @var array
73
     */
74
    protected $supplier;
75
76
    /**
77
     * @var Instance of Aws\S3\S3Client
78
     */
79
    protected $s3_client;
80
81
    /**
82
     * @var Instance of Guzzle\Batch\BatchBuilder
83
     */
84
    protected $batch;
85
86
    /**
87
     * @var \Publiux\laravelcdn\Contracts\CdnHelperInterface
88
     */
89
    protected $cdn_helper;
90
91
    /**
92
     * @var \Publiux\laravelcdn\Validators\Contracts\ConfigurationsInterface
93
     */
94
    protected $configurations;
95
96
    /**
97
     * @var \Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface
98
     */
99
    protected $provider_validator;
100
101
    /**
102
     * @param \Symfony\Component\Console\Output\ConsoleOutput                     $console
103
     * @param \Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface $provider_validator
104
     * @param \Publiux\laravelcdn\Contracts\CdnHelperInterface                    $cdn_helper
105
     */
106
    public function __construct(
107
        ConsoleOutput $console,
108
        ProviderValidatorInterface $provider_validator,
109
        CdnHelperInterface $cdn_helper
110
    ) {
111
        $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...
112
        $this->provider_validator = $provider_validator;
113
        $this->cdn_helper = $cdn_helper;
114
    }
115
116
    /**
117
     * Read the configuration and prepare an array with the relevant configurations
118
     * for the (AWS S3) provider. and return itself.
119
     *
120
     * @param $configurations
121
     *
122
     * @return $this
123
     */
124
    public function init($configurations)
125
    {
126
        // merge the received config array with the default configurations array to
127
        // fill missed keys with null or default values.
128
        $this->default = array_merge($this->default, $configurations);
129
130
        $supplier = [
131
            'provider_url'   => $this->default['url'],
132
            'threshold'      => $this->default['threshold'],
133
            'version'        => $this->default['providers']['aws']['s3']['version'],
134
            'region'         => $this->default['providers']['aws']['s3']['region'],
135
            'buckets'        => $this->default['providers']['aws']['s3']['buckets'],
136
            'acl'            => $this->default['providers']['aws']['s3']['acl'],
137
            'cloudfront'     => $this->default['providers']['aws']['s3']['cloudfront']['use'],
138
            'cloudfront_url' => $this->default['providers']['aws']['s3']['cloudfront']['cdn_url'],
139
        ];
140
141
        // check if any required configuration is missed
142
        $this->provider_validator->validate($supplier, $this->rules);
143
144
        $this->supplier = $supplier;
145
146
        return $this;
147
    }
148
149
    /**
150
     * Create an S3 client instance
151
     * (Note: it will read the credentials form the .env file).
152
     *
153
     * @return bool
154
     */
155
    public function connect()
156
    {
157
        try {
158
            // Instantiate an S3 client
159
            $this->setS3Client(new S3Client([
160
                        'version' => $this->supplier['version'],
161
                        'region'  => $this->supplier['region'],
162
                    ]
163
                )
164
            );
165
        } catch (\Exception $e) {
166
            return false;
167
        }
168
169
        return true;
170
    }
171
172
    /**
173
     * Upload assets.
174
     *
175
     * @param $assets
176
     *
177
     * @return bool
178
     */
179
    public function upload($assets)
180
    {
181
        // connect before uploading
182
        $connected = $this->connect();
183
184
        if (!$connected) {
185
            return false;
186
        }
187
188
        // user terminal message
189
        $this->console->writeln('<fg=yellow>Comparing local files and bucket...</fg=yellow>');
190
191
        $assets = $this->getFilesAlreadyOnBucket($assets);
192
193
        // upload each asset file to the CDN
194
        if (count($assets) > 0) {
195
196
            // Review files before upload if user wishes.
197
            /*$review = $this->console->option('review');
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
198
            if ($review) {
199
                $this->console->writeln('<fg=green>The files to be uploaded are....</fg=green>');
200
                foreach ($assets as $file) {
201
                    $this->console->writeln('<fg=cyan>'.$file->getRealpath().'</fg=cyan>');
202
                }
203
204
                //Ask the user to confirm that they want to continue the upload.
205
                if (!$this->console->confirm('Do you wish to continue? [y|N]')) {
206
                    $this->console->writeln('<fg=red>Upload cancelled.</fg=cyan>');
207
                    return true;
208
                }
209
            }*/
210
211
            $this->console->writeln('<fg=yellow>Upload in progress......</fg=yellow>');
212
            foreach ($assets as $file) {
213
                try {
214
                    $this->console->writeln('<fg=cyan>'.'Uploading file path: '.$file->getRealpath().'</fg=cyan>');
215
                    $command = $this->s3_client->getCommand('putObject', [
216
217
                        // the bucket name
218
                        'Bucket' => $this->getBucket(),
219
                        // the path of the file on the server (CDN)
220
                        'Key' => str_replace('\\', '/', $file->getPathName()),
221
                        // the path of the path locally
222
                        'Body' => fopen($file->getRealPath(), 'r'),
223
                        // the permission of the file
224
225
                        'ACL'          => $this->acl,
226
                        'CacheControl' => $this->default['providers']['aws']['s3']['cache-control'],
227
                        'MetaData'     => $this->default['providers']['aws']['s3']['metadata'],
228
                        'Expires'      => $this->default['providers']['aws']['s3']['expires'],
229
                    ]);
230
//                var_dump(get_class($command));exit();
0 ignored issues
show
Unused Code Comprehensibility introduced by
77% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
231
232
                    $this->s3_client->execute($command);
233
                } catch (S3Exception $e) {
234
                    $this->console->writeln('<fg=red>'.$e->getMessage().'</fg=red>');
235
236
                    return false;
237
                }
238
            }
239
240
            // user terminal message
241
            $this->console->writeln('<fg=green>Upload completed successfully.</fg=green>');
242
        } else {
243
            // user terminal message
244
            $this->console->writeln('<fg=yellow>No new files to upload.</fg=yellow>');
245
        }
246
247
        return true;
248
    }
249
250
    /**
251
     * Empty bucket.
252
     *
253
     * @return bool
254
     */
255
    public function emptyBucket()
256
    {
257
258
        // connect before uploading
259
        $connected = $this->connect();
260
261
        if (!$connected) {
262
            return false;
263
        }
264
265
        // user terminal message
266
        $this->console->writeln('<fg=yellow>Emptying in progress...</fg=yellow>');
267
268
        try {
269
270
            // Get the contents of the bucket for information purposes
271
            $contents = $this->s3_client->listObjects([
272
                'Bucket' => $this->getBucket(),
273
                'Key'    => '',
274
            ]);
275
276
            // Check if the bucket is already empty
277
            if (!$contents['Contents']) {
278
                $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is already empty.</fg=green>');
279
280
                return true;
281
            }
282
283
            // Empty out the bucket
284
            $empty = BatchDelete::fromListObjects($this->s3_client, [
285
                'Bucket' => $this->getBucket(),
286
                'Prefix' => null,
287
            ]);
288
289
            $empty->delete();
290
        } catch (S3Exception $e) {
291
            $this->console->writeln('<fg=red>'.$e->getMessage().'</fg=red>');
292
293
            return false;
294
        }
295
296
        $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is now empty.</fg=green>');
297
298
        return true;
299
    }
300
301
    /**
302
     * This function will be called from the CdnFacade class when
303
     * someone use this {{ Cdn::asset('') }} facade helper.
304
     *
305
     * @param $path
306
     *
307
     * @return string
308
     */
309
    public function urlGenerator($path)
310
    {
311
        if ($this->getCloudFront() === true) {
312
            $url = $this->cdn_helper->parseUrl($this->getCloudFrontUrl());
313
314 View Code Duplication
            if (array_key_exists('scheme', $url)) {
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...
315
                return $url['scheme'].'://'.$url['host'].'/'.$path;
316
            } else {
317
                return '//'.$url['host'].'/'.$path;
318
            }
319
        }
320
321
        $url = $this->cdn_helper->parseUrl($this->getUrl());
322
323
        $bucket = $this->getBucket();
324
        $bucket = (!empty($bucket)) ? $bucket.'.' : '';
325
326 View Code Duplication
        if (array_key_exists('scheme', $url)) {
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...
327
            return $url['scheme'].'://'.$bucket.$url['host'].'/'.$path;
328
        } else {
329
            return '//'.$bucket.$url['host'].'/'.$path;
330
        }
331
    }
332
333
    /**
334
     * @param $s3_client
335
     */
336
    public function setS3Client($s3_client)
337
    {
338
        $this->s3_client = $s3_client;
339
    }
340
341
    /**
342
     * @return string
343
     */
344
    public function getUrl()
345
    {
346
        return rtrim($this->provider_url, '/').'/';
347
    }
348
349
    /**
350
     * @return string
351
     */
352
    public function getCloudFront()
353
    {
354
        if (!is_bool($cloudfront = $this->cloudfront)) {
355
            return false;
356
        }
357
358
        return $cloudfront;
359
    }
360
361
    /**
362
     * @return string
363
     */
364
    public function getCloudFrontUrl()
365
    {
366
        return rtrim($this->cloudfront_url, '/').'/';
367
    }
368
369
    /**
370
     * @return array
371
     */
372
    public function getBucket()
373
    {
374
        // this step is very important, "always assign returned array from
375
        // magical function to a local variable if you need to modify it's
376
        // state or apply any php function on it." because the returned is
377
        // a copy of the original variable. this prevent this error:
378
        // Indirect modification of overloaded property
379
        // Publiux\laravelcdn\Providers\AwsS3Provider::$buckets has no effect
380
        $bucket = $this->buckets;
381
382
        return rtrim(key($bucket), '/');
383
    }
384
385
    /**
386
     * @param $attr
387
     *
388
     * @return Mix | null
389
     */
390
    public function __get($attr)
391
    {
392
        return isset($this->supplier[$attr]) ? $this->supplier[$attr] : null;
393
    }
394
395
    /**
396
     * @param $assets
397
     *
398
     * @return mixed
399
     */
400
    private function getFilesAlreadyOnBucket($assets)
401
    {
402
        $filesOnAWS = new Collection([]);
403
404
        $files = $this->s3_client->listObjects([
405
            'Bucket' => $this->getBucket(),
406
        ]);
407
408
        if (!$files['Contents']) {
409
            //no files on bucket. lets upload everything found.
410
            return $assets;
411
        }
412
413
        foreach ($files['Contents'] as $file) {
414
            $a = ['Key' => $file['Key'], 'LastModified' => $file['LastModified']->getTimestamp(), 'Size' => $file['Size']];
415
            $filesOnAWS->put($file['Key'], $a);
416
        }
417
418
        $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...
419
            $fileOnAWS = $filesOnAWS->get(str_replace('\\', '/', $item->getPathName()));
420
421
            //select to upload files that are different in size AND last modified time.
422
            if (!($item->getMTime() === $fileOnAWS['LastModified']) && !($item->getSize() === $fileOnAWS['Size'])) {
423
                return $item;
424
            }
425
        });
426
427
        $assets = $assets->reject(function ($item) {
428
            return $item === null;
429
        });
430
431
        return $assets;
432
    }
433
}
434