AwsS3Provider::getFilesAlreadyOnBucket()   B
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 33
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

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