Completed
Pull Request — master (#43)
by
unknown
01:36
created

AwsS3Provider::getUploadFolderForFile()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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