Completed
Pull Request — master (#45)
by Raimondas
01:40
created

AwsS3Provider::getCloudFrontUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
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 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  $use_path_style_endpoint
32
 * @property string $http
33
 *
34
 * @author   Mahmoud Zalt <[email protected]>
35
 */
36
class AwsS3Provider extends Provider implements ProviderInterface
37
{
38
    /**
39
     * All the configurations needed by this class with the
40
     * optional configurations default values.
41
     *
42
     * @var array
43
     */
44
    protected $default = [
45
        'url' => null,
46
        'threshold' => 10,
47
        'providers' => [
48
            'aws' => [
49
                's3' => [
50
                    'version' => null,
51
                    'region' => null,
52
                    'endpoint' => null,
53
                    'buckets' => null,
54
                    'upload_folder' => '',
55
                    'http' => null,
56
                    'acl' => 'public-read',
57
                    'cloudfront' => [
58
                        'use' => false,
59
                        'cdn_url' => null,
60
                    ],
61
                    'use_path_style_endpoint' => false
62
                ],
63
            ],
64
        ],
65
    ];
66
67
    /**
68
     * Required configurations (must exist in the config file).
69
     *
70
     * @var array
71
     */
72
    protected $rules = ['version', 'region', 'key', 'secret', 'buckets', 'url'];
73
74
    /**
75
     * this array holds the parsed configuration to be used across the class.
76
     *
77
     * @var Array
78
     */
79
    protected $supplier;
80
81
    /**
82
     * @var Instance of Aws\S3\S3Client
83
     */
84
    protected $s3_client;
85
86
    /**
87
     * @var Instance of Guzzle\Batch\BatchBuilder
88
     */
89
    protected $batch;
90
91
    /**
92
     * @var \Publiux\laravelcdn\Contracts\CdnHelperInterface
93
     */
94
    protected $cdn_helper;
95
96
    /**
97
     * @var \Publiux\laravelcdn\Validators\Contracts\ConfigurationsInterface
98
     */
99
    protected $configurations;
100
101
    /**
102
     * @var \Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface
103
     */
104
    protected $provider_validator;
105
106
    /**
107
     * @param \Symfony\Component\Console\Output\ConsoleOutput $console
108
     * @param \Publiux\laravelcdn\Validators\Contracts\ProviderValidatorInterface $provider_validator
109
     * @param \Publiux\laravelcdn\Contracts\CdnHelperInterface                    $cdn_helper
110
     */
111
    public function __construct(
112
        ConsoleOutput $console,
113
        ProviderValidatorInterface $provider_validator,
114
        CdnHelperInterface $cdn_helper
115
    ) {
116
        $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...
117
        $this->provider_validator = $provider_validator;
118
        $this->cdn_helper = $cdn_helper;
119
    }
120
121
    /**
122
     * Read the configuration and prepare an array with the relevant configurations
123
     * for the (AWS S3) provider. and return itself.
124
     *
125
     * @param $configurations
126
     *
127
     * @return $this
128
     */
129
    public function init($configurations)
130
    {
131
        // merge the received config array with the default configurations array to
132
        // fill missed keys with null or default values.
133
        $this->default = array_replace_recursive($this->default, $configurations);
134
135
        $supplier = [
136
            'provider_url' => $this->default['url'],
137
            'threshold' => $this->default['threshold'],
138
            'version' => $this->default['providers']['aws']['s3']['version'],
139
            'region' => $this->default['providers']['aws']['s3']['region'],
140
            'endpoint' => $this->default['providers']['aws']['s3']['endpoint'],
141
            'buckets' => $this->default['providers']['aws']['s3']['buckets'],
142
            'acl' => $this->default['providers']['aws']['s3']['acl'],
143
            'cloudfront' => $this->default['providers']['aws']['s3']['cloudfront']['use'],
144
            'cloudfront_url' => $this->default['providers']['aws']['s3']['cloudfront']['cdn_url'],
145
            'http' => $this->default['providers']['aws']['s3']['http'],
146
            'upload_folder' => $this->default['providers']['aws']['s3']['upload_folder'],
147
            'use_path_style_endpoint' => $this->default['providers']['aws']['s3']['use_path_style_endpoint']
148
        ];
149
150
        // check if any required configuration is missed
151
        $this->provider_validator->validate($supplier, $this->rules);
152
153
        $this->supplier = $supplier;
154
155
        return $this;
156
    }
157
158
    /**
159
     * Upload assets.
160
     *
161
     * @param $assets
162
     *
163
     * @return bool
164
     */
165
    public function upload($assets)
166
    {
167
        // connect before uploading
168
        $connected = $this->connect();
169
170
        if (!$connected) {
171
            return false;
172
        }
173
174
        // user terminal message
175
        $this->console->writeln('<fg=yellow>Comparing local files and bucket...</fg=yellow>');
176
177
        $assets = $this->getFilesAlreadyOnBucket($assets);
178
179
        // upload each asset file to the CDN
180
        if (count($assets) > 0) {
181
            $this->console->writeln('<fg=yellow>Upload in progress......</fg=yellow>');
182
            foreach ($assets as $file) {
183
                try {
184
                    $this->console->writeln('<fg=cyan>'.'Uploading file path: '.$file->getRealpath().'</fg=cyan>');
185
                    $command = $this->s3_client->getCommand('putObject', [
186
187
                        // the bucket name
188
                        'Bucket' => $this->getBucket(),
189
                        // the path of the file on the server (CDN)
190
                        'Key' => $this->supplier['upload_folder'] . str_replace('\\', '/', $file->getPathName()),
191
                        // the path of the path locally
192
                        'Body' => fopen($file->getRealPath(), 'r'),
193
                        // the permission of the file
194
195
                        'ACL' => $this->acl,
196
                        'CacheControl' => $this->default['providers']['aws']['s3']['cache-control'],
197
                        'Metadata' => $this->default['providers']['aws']['s3']['metadata'],
198
                        'Expires' => $this->default['providers']['aws']['s3']['expires'],
199
                    ]);
200
//                var_dump(get_class($command));exit();
201
202
203
                    $this->s3_client->execute($command);
204
                } catch (S3Exception $e) {
205
                    $this->console->writeln('<fg=red>Upload error: '.$e->getMessage().'</fg=red>');
206
                    return false;
207
                }
208
            }
209
210
            // user terminal message
211
            $this->console->writeln('<fg=green>Upload completed successfully.</fg=green>');
212
        } else {
213
            // user terminal message
214
            $this->console->writeln('<fg=yellow>No new files to upload.</fg=yellow>');
215
        }
216
217
        return true;
218
    }
219
220
    /**
221
     * Create an S3 client instance
222
     * (Note: it will read the credentials form the .env file).
223
     *
224
     * @return bool
225
     */
226
    public function connect()
227
    {
228
        try {
229
            // Instantiate an S3 client
230
            $this->setS3Client(new S3Client([
231
                        'version' => $this->supplier['version'],
232
                        'region' => $this->supplier['region'],
233
                        'endpoint' => $this->supplier['endpoint'],
234
                        'http' => $this->supplier['http'],
235
                        'use_path_style_endpoint' => $this->supplier['use_path_style_endpoint']
236
                    ]
237
                )
238
            );
239
        } catch (\Exception $e) {
240
            $this->console->writeln('<fg=red>Connection error: '.$e->getMessage().'</fg=red>');
241
            return false;
242
        }
243
244
        return true;
245
    }
246
247
    /**
248
     * @param $s3_client
249
     */
250
    public function setS3Client($s3_client)
251
    {
252
        $this->s3_client = $s3_client;
253
    }
254
255
    /**
256
     * @param $assets
257
     * @return mixed
258
     */
259
    private function getFilesAlreadyOnBucket($assets)
260
    {
261
        $filesOnAWS = new Collection([]);
262
263
        $files = $this->s3_client->listObjects([
264
            'Bucket' => $this->getBucket(),
265
        ]);
266
267
        if (!$files['Contents']) {
268
            //no files on bucket. lets upload everything found.
269
            return $assets;
270
        }
271
272
        foreach ($files['Contents'] as $file) {
273
            $a = [
274
                'Key' => $file['Key'],
275
                "LastModified" => $file['LastModified']->getTimestamp(),
276
                'Size' => $file['Size']
277
            ];
278
            $filesOnAWS->put($file['Key'], $a);
279
        }
280
281
        $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...
282
            $fileOnAWS = $filesOnAWS->get(str_replace('\\', '/', $item->getPathName()));
283
284
            //select to upload files that are different in size AND last modified time.
285
            if (!($item->getMTime() === $fileOnAWS['LastModified']) && !($item->getSize() === $fileOnAWS['Size'])) {
286
                return $item;
287
            }
288
        });
289
290
        $assets = $assets->reject(function ($item) {
291
            return $item === null;
292
        });
293
294
        return $assets;
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
383
        if ($this->supplier['use_path_style_endpoint']) {
384
            $bucket = (!empty($bucket)) ? $bucket.'/' : '';
385
            if (isset($url['port'])) {
386
                $port = (
387
                    ($url['scheme'] == 'https' && $url['port'] == 443) ||
388
                    ($url['scheme'] == 'http' && $url['port'] == 80)
389
                ) ? '' : ':' . $url['port'];
390
            } else {
391
                $port = '';
392
            }
393
            return $url['scheme'] . '://' .  $url['host'] . $port . '/' . $bucket . $path;
394
        }
395
396
        $bucket = (!empty($bucket)) ? $bucket.'.' : '';
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