Completed
Push — master ( ea324c...2e8bb7 )
by Andrey
01:33
created

S3Upload::getS3FileOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
c 0
b 0
f 0
rs 10
nc 1
cc 1
nop 0
1
<?php
2
3
namespace Itstructure\MFUploader\models\upload;
4
5
use yii\imagine\Image;
6
use yii\db\ActiveRecord;
7
use yii\base\{InvalidConfigException, InvalidValueException};
8
use yii\helpers\{ArrayHelper, Inflector};
9
use yii\web\UploadedFile;
10
use Aws\S3\{S3ClientInterface, S3MultiRegionClient};
11
use Itstructure\MFUploader\models\S3FileOptions;
12
use Itstructure\MFUploader\Module;
13
use Itstructure\MFUploader\models\Mediafile;
14
use Itstructure\MFUploader\interfaces\{ThumbConfigInterface, UploadModelInterface};
15
16
/**
17
 * Class S3Upload
18
 *
19
 * @property string $s3DefaultBucket Amazon web services S3 default bucket for upload files (not for delete).
20
 * @property array $s3Buckets Buckets for upload depending on the owner.
21
 * @property S3MultiRegionClient|S3ClientInterface $s3Client Amazon web services SDK S3 client.
22
 * @property string $originalContent Binary contente of the original file.
23
 * @property array $objectsForDelete Objects for delete (files in the S3 directory).
24
 * @property string $bucketForDelete Bucket, in which the located files will be deleted.
25
 * @property string $bucketForUpload Bucket for upload new files.
26
 * @property ActiveRecord|S3FileOptions $s3FileOptions S3 file options (bucket, prefix).
27
 * @property Mediafile $mediafileModel Mediafile model to save files data.
28
 * @property UploadedFile $file File object.
29
 *
30
 * @package Itstructure\MFUploader\models
31
 *
32
 * @author Andrey Girnik <[email protected]>
33
 */
34
class S3Upload extends BaseUpload implements UploadModelInterface
35
{
36
    const DIR_LENGTH_FIRST = 2;
37
    const DIR_LENGTH_SECOND = 4;
38
39
    const BUCKET_DIR_SEPARATOR = '/';
40
41
    /**
42
     * Amazon web services S3 default bucket for upload files (not for delete).
43
     *
44
     * @var string
45
     */
46
    public $s3DefaultBucket;
47
48
    /**
49
     * Buckets for upload depending on the owner.
50
     *
51
     * @var array
52
     */
53
    public $s3Buckets = [];
54
55
    /**
56
     * Amazon web services SDK S3 client.
57
     *
58
     * @var S3ClientInterface|S3MultiRegionClient
59
     */
60
    private $s3Client;
61
62
    /**
63
     * Binary contente of the original file.
64
     *
65
     * @var string
66
     */
67
    private $originalContent;
68
69
    /**
70
     * Objects for delete (files in the S3 directory).
71
     *
72
     * @var array
73
     */
74
    private $objectsForDelete = [];
75
76
    /**
77
     * Bucket, in which the located files will be deleted.
78
     *
79
     * @var string
80
     */
81
    private $bucketForDelete;
82
83
    /**
84
     * Bucket for upload new files.
85
     *
86
     * @var string
87
     */
88
    private $bucketForUpload;
89
90
    /**
91
     * S3 file options (bucket, prefix).
92
     *
93
     * @var ActiveRecord|S3FileOptions
94
     */
95
    private $s3FileOptions;
96
97
    /**
98
     * Initialize.
99
     */
100
    public function init()
101
    {
102
        if (null === $this->s3Client) {
103
            throw new InvalidConfigException('S3 client is not defined correctly.');
104
        }
105
    }
106
107
    /**
108
     * Set s3 client.
109
     *
110
     * @param S3ClientInterface $s3Client
111
     */
112
    public function setS3Client(S3ClientInterface $s3Client): void
113
    {
114
        $this->s3Client = $s3Client;
0 ignored issues
show
Documentation Bug introduced by
It seems like $s3Client of type object<Aws\S3\S3ClientInterface> is incompatible with the declared type object<S3ClientInterface...ct<S3MultiRegionClient> of property $s3Client.

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...
115
    }
116
117
    /**
118
     * Get s3 client.
119
     *
120
     * @return S3ClientInterface|null
121
     */
122
    public function getS3Client()
123
    {
124
        return $this->s3Client;
125
    }
126
127
    /**
128
     * Get storage type - aws.
129
     *
130
     * @return string
131
     */
132
    protected function getStorageType(): string
133
    {
134
        return Module::STORAGE_TYPE_S3;
135
    }
136
137
    /**
138
     * Set some params for upload.
139
     * It is needed to set the next parameters:
140
     * $this->uploadDir
141
     * $this->outFileName
142
     * $this->bucketForUpload
143
     *
144
     * @throws InvalidConfigException
145
     *
146
     * @return void
147
     */
148
    protected function setParamsForSend(): void
149
    {
150
        $uploadDir = $this->getUploadDirConfig($this->file->type);
151
        $uploadDir = trim(str_replace('\\', self::BUCKET_DIR_SEPARATOR, $uploadDir), self::BUCKET_DIR_SEPARATOR);
152
153
        if (!empty($this->subDir)) {
154
            $uploadDir = $uploadDir .
155
                self::BUCKET_DIR_SEPARATOR .
156
                trim(str_replace('\\', self::BUCKET_DIR_SEPARATOR, $this->subDir), self::BUCKET_DIR_SEPARATOR);
157
        }
158
159
        $this->uploadDir = $uploadDir .
160
            self::BUCKET_DIR_SEPARATOR . substr(md5(time()), 0, self::DIR_LENGTH_FIRST) .
161
            self::BUCKET_DIR_SEPARATOR . substr(md5(microtime().$this->file->tempName), 0, self::DIR_LENGTH_SECOND);
162
163
        $this->outFileName = $this->renameFiles ?
164
            md5(md5(microtime()).$this->file->tempName).'.'.$this->file->extension :
165
            Inflector::slug($this->file->baseName).'.'. $this->file->extension;
166
167
        $this->bucketForUpload = null !== $this->owner && isset($this->s3Buckets[$this->owner]) ?
168
            $this->s3Buckets[$this->owner] : $this->s3DefaultBucket;
169
    }
170
171
    /**
172
     * Set some params for delete.
173
     * It is needed to set the next parameters:
174
     * $this->objectsForDelete
175
     * $this->bucketForDelete
176
     *
177
     * @return void
178
     */
179
    protected function setParamsForDelete(): void
180
    {
181
        $s3fileOptions = $this->getS3FileOptions();
182
        
183
        $objects = $this->s3Client->listObjects([
184
            'Bucket' => $s3fileOptions->bucket,
185
            'Prefix' => $s3fileOptions->prefix
186
        ]);
187
188
        $this->objectsForDelete = null === $objects['Contents'] ? [] : array_map(function ($item) {
189
            return [
190
                'Key' => $item
191
            ];
192
        }, ArrayHelper::getColumn($objects['Contents'], 'Key'));
193
194
        $this->bucketForDelete = $s3fileOptions->bucket;
195
    }
196
197
    /**
198
     * Send file to remote storage.
199
     *
200
     * @throws InvalidConfigException
201
     *
202
     * @return bool
203
     */
204
    protected function sendFile(): bool
205
    {
206
        if (null === $this->bucketForUpload || !is_string($this->bucketForUpload)) {
207
            throw new InvalidConfigException('S3 bucket for upload is not defined correctly.');
208
        }
209
210
        $result = $this->s3Client->putObject([
211
            'ACL' => 'public-read',
212
            'SourceFile' => $this->file->tempName,
213
            'Key' => $this->uploadDir . self::BUCKET_DIR_SEPARATOR . $this->outFileName,
214
            'Bucket' => $this->bucketForUpload
215
        ]);
216
217
        if ($result['ObjectURL']) {
218
            $this->databaseUrl = $result['ObjectURL'];
219
            return true;
220
        }
221
222
        return false;
223
    }
224
225
    /**
226
     * Delete storage directory with original file and thumbs.
227
     *
228
     * @return void
229
     */
230
    protected function deleteFiles(): void
231
    {
232
        if (count($this->objectsForDelete) > 0) {
233
            $this->s3Client->deleteObjects([
234
                'Bucket' => $this->bucketForDelete,
235
                'Delete' => [
236
                    'Objects' => $this->objectsForDelete,
237
                ]
238
            ]);
239
        }
240
    }
241
242
    /**
243
     * Create thumb.
244
     *
245
     * @param ThumbConfigInterface $thumbConfig
246
     *
247
     * @return mixed
248
     */
249
    protected function createThumb(ThumbConfigInterface $thumbConfig)
250
    {
251
        $originalFile = pathinfo($this->mediafileModel->url);
252
253
        if (null === $this->s3FileOptions) {
254
            $this->s3FileOptions = $this->getS3FileOptions();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getS3FileOptions() of type array or boolean is incompatible with the declared type object<yii\db\ActiveReco...r\models\S3FileOptions> of property $s3FileOptions.

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...
255
        }
256
257
        $uploadThumbUrl = $this->s3FileOptions->prefix .
258
                    self::BUCKET_DIR_SEPARATOR .
259
                    $this->getThumbFilename($originalFile['filename'],
260
                        $originalFile['extension'],
261
                        $thumbConfig->getAlias(),
262
                        $thumbConfig->getWidth(),
263
                        $thumbConfig->getHeight()
264
                    );
265
266
        $thumbContent = Image::thumbnail(Image::getImagine()->load($this->getOriginalContent()),
267
            $thumbConfig->getWidth(),
268
            $thumbConfig->getHeight(),
269
            $thumbConfig->getMode()
270
        )->get($originalFile['extension']);
271
272
        $result = $this->s3Client->putObject([
273
            'ACL' => 'public-read',
274
            'Body' => $thumbContent,
275
            'Key' => $uploadThumbUrl,
276
            'Bucket' => $this->s3FileOptions->bucket
277
        ]);
278
279
        if ($result['ObjectURL'] && !empty($result['ObjectURL'])) {
280
            return $result['ObjectURL'];
281
        }
282
283
        return null;
284
    }
285
286
    /**
287
     * Actions after main save.
288
     *
289
     * @return mixed
290
     */
291
    protected function afterSave()
292
    {
293 View Code Duplication
        if (null !== $this->owner && null !== $this->ownerId && null != $this->ownerAttribute) {
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...
294
            $this->mediafileModel->addOwner($this->ownerId, $this->owner, $this->ownerAttribute);
295
        }
296
297
        if (null !== $this->bucketForUpload && null !== $this->uploadDir) {
298
            $this->setS3FileOptions($this->bucketForUpload, $this->uploadDir);
299
        }
300
    }
301
302
    /**
303
     * Get binary contente of the original file.
304
     *
305
     * @throws InvalidValueException
306
     *
307
     * @return string
308
     */
309
    private function getOriginalContent()
310
    {
311
        if (null === $this->originalContent) {
312
            $this->originalContent = file_get_contents($this->mediafileModel->url);
313
        }
314
315
        if (!$this->originalContent) {
316
            throw new InvalidValueException('Content from '.$this->mediafileModel->url.' can not be read.');
317
        }
318
319
        return $this->originalContent;
320
    }
321
322
    /**
323
     * S3 file options (bucket, prefix).
324
     *
325
     * @return ActiveRecord|S3FileOptions
326
     */
327
    private function getS3FileOptions()
328
    {
329
        return S3FileOptions::find()->where([
330
            'mediafileId' => $this->mediafileModel->id
331
        ])->one();
332
    }
333
334
    /**
335
     * Set S3 options for uploaded file in amazon S3 storage.
336
     *
337
     * @param string $bucket
338
     * @param string $prefix
339
     *
340
     * @return void
341
     */
342
    private function setS3FileOptions(string $bucket, string $prefix): void
343
    {
344
        if (null !== $this->file) {
345
            S3FileOptions::deleteAll([
346
                'mediafileId' => $this->mediafileModel->id
347
            ]);
348
            $optionsModel = new S3FileOptions();
349
            $optionsModel->mediafileId = $this->mediafileModel->id;
350
            $optionsModel->bucket = $bucket;
351
            $optionsModel->prefix = $prefix;
352
            $optionsModel->save();
353
        }
354
    }
355
}
356