Issues (46)

src/models/upload/S3Upload.php (2 issues)

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};
0 ignored issues
show
The type yii\base\InvalidValueException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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;
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();
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
        if (null !== $this->owner && null !== $this->ownerId && null != $this->ownerAttribute) {
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([
0 ignored issues
show
Bug Best Practice introduced by
The expression return Itstructure\MFUpl...afileModel->id))->one() also could return the type array which is incompatible with the documented return type Itstructure\MFUploader\m...ons|yii\db\ActiveRecord.
Loading history...
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