Completed
Push — master ( f5da05...c7bc08 )
by
unknown
05:30
created

AwsS3Provider::getFilesAlreadyOnBucket()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 16
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 33
rs 8.439
1
<?php
2
3
namespace Vinelab\Cdn\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 Symfony\Component\Console\Output\ConsoleOutput;
10
use Vinelab\Cdn\Contracts\CdnHelperInterface;
11
use Vinelab\Cdn\Providers\Contracts\ProviderInterface;
12
use Vinelab\Cdn\Validators\Contracts\ProviderValidatorInterface;
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
 */
34
class AwsS3Provider extends Provider implements ProviderInterface
35
{
36
    /**
37
     * All the configurations needed by this class with the
38
     * optional configurations default values.
39
     *
40
     * @var array
41
     */
42
    protected $default = [
43
        'url' => null,
44
        'threshold' => 10,
45
        'providers' => [
46
            'aws' => [
47
                's3' => [
48
                    'version' => null,
49
                    'region' => null,
50
                    'buckets' => null,
51
                    'acl' => 'public-read',
52
                    'cloudfront' => [
53
                        'use' => false,
54
                        'cdn_url' => null,
55
                    ],
56
                ],
57
            ],
58
        ],
59
    ];
60
61
    /**
62
     * Required configurations (must exist in the config file).
63
     *
64
     * @var array
65
     */
66
    protected $rules = ['version', 'region', 'key', 'secret', 'buckets', 'url'];
67
68
    /**
69
     * this array holds the parsed configuration to be used across the class.
70
     *
71
     * @var Array
72
     */
73
    protected $supplier;
74
75
    /**
76
     * @var Instance of Aws\S3\S3Client
77
     */
78
    protected $s3_client;
79
80
    /**
81
     * @var Instance of Guzzle\Batch\BatchBuilder
82
     */
83
    protected $batch;
84
85
    /**
86
     * @var \Vinelab\Cdn\Contracts\CdnHelperInterface
87
     */
88
    protected $cdn_helper;
89
90
    /**
91
     * @var \Vinelab\Cdn\Validators\Contracts\ConfigurationsInterface
92
     */
93
    protected $configurations;
94
95
    /**
96
     * @var \Vinelab\Cdn\Validators\Contracts\ProviderValidatorInterface
97
     */
98
    protected $provider_validator;
99
100
    /**
101
     * @param \Symfony\Component\Console\Output\ConsoleOutput              $console
102
     * @param \Vinelab\Cdn\Validators\Contracts\ProviderValidatorInterface $provider_validator
103
     * @param \Vinelab\Cdn\Contracts\CdnHelperInterface                    $cdn_helper
104
     */
105
    public function __construct(
106
        ConsoleOutput $console,
107
        ProviderValidatorInterface $provider_validator,
108
        CdnHelperInterface $cdn_helper
109
    ) {
110
        $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<Vinelab\Cdn\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...
111
        $this->provider_validator = $provider_validator;
112
        $this->cdn_helper = $cdn_helper;
113
    }
114
115
    /**
116
     * Read the configuration and prepare an array with the relevant configurations
117
     * for the (AWS S3) provider. and return itself.
118
     *
119
     * @param $configurations
120
     *
121
     * @return $this
122
     */
123
    public function init($configurations)
124
    {
125
        // merge the received config array with the default configurations array to
126
        // fill missed keys with null or default values.
127
        $this->default = array_merge($this->default, $configurations);
128
129
        $supplier = [
130
            'provider_url' => $this->default['url'],
131
            'threshold' => $this->default['threshold'],
132
            'version' => $this->default['providers']['aws']['s3']['version'],
133
            'region' => $this->default['providers']['aws']['s3']['region'],
134
            'buckets' => $this->default['providers']['aws']['s3']['buckets'],
135
            'acl' => $this->default['providers']['aws']['s3']['acl'],
136
            'cloudfront' => $this->default['providers']['aws']['s3']['cloudfront']['use'],
137
            'cloudfront_url' => $this->default['providers']['aws']['s3']['cloudfront']['cdn_url'],
138
        ];
139
140
        // check if any required configuration is missed
141
        $this->provider_validator->validate($supplier, $this->rules);
142
143
        $this->supplier = $supplier;
144
145
        return $this;
146
    }
147
148
    /**
149
     * Create an S3 client instance
150
     * (Note: it will read the credentials form the .env file).
151
     *
152
     * @return bool
153
     */
154
    public function connect()
155
    {
156
        try {
157
            // Instantiate an S3 client
158
            $this->setS3Client(new S3Client([
159
                        'version' => $this->supplier['version'],
160
                        'region' => $this->supplier['region'],
161
                    ]
162
                )
163
            );
164
        } catch (\Exception $e) {
165
            return false;
166
        }
167
168
        return true;
169
    }
170
171
    /**
172
     * Upload assets.
173
     *
174
     * @param $assets
175
     *
176
     * @return bool
177
     */
178
    public function upload($assets)
179
    {
180
        // connect before uploading
181
        $connected = $this->connect();
182
183
        if (!$connected) {
184
            return false;
185
        }
186
187
        // user terminal message
188
        $this->console->writeln('<fg=yellow>Comparing local files and bucket...</fg=yellow>');
189
190
        $assets = $this->getFilesAlreadyOnBucket($assets);
191
192
        // upload each asset file to the CDN
193
        if(count($assets) > 0) {
194
            $this->console->writeln('<fg=yellow>Upload in progress......</fg=yellow>');
195
            foreach ($assets as $file) {
196
                try {
197
                    $this->console->writeln('<fg=cyan>'.'Uploading file path: '.$file->getRealpath().'</fg=cyan>');
198
                    $command = $this->s3_client->getCommand('putObject', [
199
200
                        // the bucket name
201
                        'Bucket' => $this->getBucket(),
202
                        // the path of the file on the server (CDN)
203
                        'Key' => str_replace('\\', '/', $file->getPathName()),
204
                        // the path of the path locally
205
                        'Body' => fopen($file->getRealPath(), 'r'),
206
                        // the permission of the file
207
208
                        'ACL' => $this->acl,
209
                        'CacheControl' => $this->default['providers']['aws']['s3']['cache-control'],
210
                        'MetaData' => $this->default['providers']['aws']['s3']['metadata'],
211
                        'Expires' => $this->default['providers']['aws']['s3']['expires'],
212
                    ]);
213
//                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...
214
215
216
                    $this->s3_client->execute($command);
217
                } catch (S3Exception $e) {
218
                    $this->console->writeln('<fg=red>'.$e->getMessage().'</fg=red>');
219
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
     * Empty bucket.
236
     *
237
     * @return bool
238
     */
239
    public function emptyBucket()
240
    {
241
242
        // connect before uploading
243
        $connected = $this->connect();
244
245
        if (!$connected) {
246
            return false;
247
        }
248
249
        // user terminal message
250
        $this->console->writeln('<fg=yellow>Emptying in progress...</fg=yellow>');
251
252
        try {
253
254
            // Get the contents of the bucket for information purposes
255
            $contents = $this->s3_client->listObjects([
256
                'Bucket' => $this->getBucket(),
257
                'Key' => '',
258
            ]);
259
260
            // Check if the bucket is already empty
261
            if (!$contents['Contents']) {
262
                $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is already empty.</fg=green>');
263
264
                return true;
265
            }
266
267
            // Empty out the bucket
268
            $empty = BatchDelete::fromListObjects($this->s3_client, [
269
                'Bucket' => $this->getBucket(),
270
                'Prefix' => null,
271
            ]);
272
273
            $empty->delete();
274
        } catch (S3Exception $e) {
275
            $this->console->writeln('<fg=red>'.$e->getMessage().'</fg=red>');
276
277
            return false;
278
        }
279
280
        $this->console->writeln('<fg=green>The bucket '.$this->getBucket().' is now empty.</fg=green>');
281
282
        return true;
283
    }
284
285
    /**
286
     * This function will be called from the CdnFacade class when
287
     * someone use this {{ Cdn::asset('') }} facade helper.
288
     *
289
     * @param $path
290
     *
291
     * @return string
292
     */
293
    public function urlGenerator($path)
294
    {
295
        if ($this->getCloudFront() === true) {
296
            $url = $this->cdn_helper->parseUrl($this->getCloudFrontUrl());
297
298
            return $url['scheme'].'://'.$url['host'].'/'.$path;
299
        }
300
301
        $url = $this->cdn_helper->parseUrl($this->getUrl());
302
303
        $bucket = $this->getBucket();
304
        $bucket = (!empty($bucket)) ? $bucket.'.' : '';
305
306
        return $url['scheme'].'://'.$bucket.$url['host'].'/'.$path;
307
    }
308
309
    /**
310
     * @param $s3_client
311
     */
312
    public function setS3Client($s3_client)
313
    {
314
        $this->s3_client = $s3_client;
315
    }
316
317
    /**
318
     * @return string
319
     */
320
    public function getUrl()
321
    {
322
        return rtrim($this->provider_url, '/').'/';
323
    }
324
325
    /**
326
     * @return string
327
     */
328
    public function getCloudFront()
329
    {
330
        if (!is_bool($cloudfront = $this->cloudfront)) {
331
            return false;
332
        }
333
334
        return $cloudfront;
335
    }
336
337
    /**
338
     * @return string
339
     */
340
    public function getCloudFrontUrl()
341
    {
342
        return rtrim($this->cloudfront_url, '/').'/';
343
    }
344
345
    /**
346
     * @return array
347
     */
348
    public function getBucket()
349
    {
350
        // this step is very important, "always assign returned array from
351
        // magical function to a local variable if you need to modify it's
352
        // state or apply any php function on it." because the returned is
353
        // a copy of the original variable. this prevent this error:
354
        // Indirect modification of overloaded property
355
        // Vinelab\Cdn\Providers\AwsS3Provider::$buckets has no effect
356
        $bucket = $this->buckets;
357
358
        return rtrim(key($bucket), '/');
359
    }
360
361
    /**
362
     * @param $attr
363
     *
364
     * @return Mix | null
365
     */
366
    public function __get($attr)
367
    {
368
        return isset($this->supplier[$attr]) ? $this->supplier[$attr] : null;
369
    }
370
371
    /**
372
     * @param $assets
373
     * @return mixed
374
     */
375
    private function getFilesAlreadyOnBucket($assets)
376
    {
377
        $filesOnAWS = new Collection([]);
378
379
        $files = $this->s3_client->listObjects([
380
            'Bucket' => $this->getBucket(),
381
        ]);
382
383
        if (!$files['Contents']) {
384
            //no files on bucket. lets upload everything found.
385
            return $assets;
386
        }
387
388
        foreach($files['Contents'] as $file) {
389
            $a = ['Key' => $file['Key'], "LastModified" => $file['LastModified']->getTimestamp(), 'Size' => $file['Size']];
390
            $filesOnAWS->put($file['Key'], $a);
391
        }
392
393
        $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...
394
            $fileOnAWS = $filesOnAWS->get(str_replace('\\', '/', $item->getPathName()));
395
396
            //select to upload files that are different in size AND last modified time.
397
            if(!($item->getMTime() === $fileOnAWS['LastModified']) && !($item->getSize() === $fileOnAWS['Size'])) {
398
                return $item;
399
            }
400
        });
401
402
        $assets = $assets->reject(function($item) {
403
            return $item === null;
404
        });
405
406
        return $assets;
407
    }
408
}
409