├── .gitignore ├── README.md ├── README_RU.md ├── assets ├── yii2-floor12-files-block.css ├── yii2-floor12-files-block.css.map ├── yii2-floor12-files-block.js ├── yii2-floor12-files-block.scss ├── yii2-floor12-files.css ├── yii2-floor12-files.js └── yii2-floor12-lightbox-params.js ├── composer.json ├── phpunit.xml.dist ├── src ├── Module.php ├── actions │ ├── GetFileAction.php │ └── GetPreviewAction.php ├── assets │ ├── CropperAsset.php │ ├── FileInputWidgetAsset.php │ ├── FileListAsset.php │ ├── IconHelper.php │ ├── LightboxAsset.php │ └── SimpleAjaxUploaderAsset.php ├── components │ ├── FileBehaviour.php │ ├── FileInputWidget.php │ ├── FileListWidget.php │ ├── PictureListWidget.php │ ├── PictureWidget.php │ ├── SimpleImage.php │ ├── VideoWidget.php │ └── views │ │ ├── _fileListWidget.php │ │ ├── fileListWidget.php │ │ ├── mediaPictureWidget.php │ │ ├── multiFileInputWidget.php │ │ ├── pictureWidget.php │ │ └── singleFileInputWidget.php ├── controllers │ ├── ConsoleController.php │ └── DefaultController.php ├── logic │ ├── ClassnameEncoder.php │ ├── FileAlt.php │ ├── FileCreateFromInstance.php │ ├── FileCreateFromPath.php │ ├── FileCropRotate.php │ ├── FileRename.php │ ├── FileResize.php │ ├── ImagePreviewer.php │ ├── PathGenerator.php │ └── VideoFrameExtractor.php ├── messages │ └── ru │ │ └── files.php ├── migrations │ ├── m180627_121715_files.php │ ├── m190803_101632_alter_file.php │ └── m230602_185211_add_alt_to_file.php ├── models │ ├── File.php │ ├── FileType.php │ └── VideoStatus.php └── views │ └── default │ ├── _cropper.php │ ├── _file.php │ └── _single.php └── tests ├── TestCase.php ├── bootstrap.php ├── data ├── TestModel.php ├── graphic.jpeg ├── graphic.png ├── graphic_alpha.png ├── m180627_121715_files.php ├── photo.jpeg ├── photo.png └── testImage.jpg ├── logic ├── ClassnameEncoderTest.php ├── FileCreateFromInstanceTest.php ├── FileCreateFromPathTest.php ├── FileRenameTest.php └── PathGeneratorTest.php └── storage └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | nbproject 3 | .buildpath 4 | .project 5 | .settings 6 | Thumbs.db 7 | .DS_Store 8 | /vendor 9 | phpunit.xml 10 | tests/_output/* 11 | composer.lock 12 | assets/.sass-cache -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yii2-module-files 2 | 3 | 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/floor12/yii2-module-files/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/floor12/yii2-module-files/?branch=master) 5 | [![Latest Stable Version](https://poser.pugx.org/floor12/yii2-module-files/v/stable)](https://packagist.org/packages/floor12/yii2-module-files) 6 | [![Latest Unstable Version](https://poser.pugx.org/floor12/yii2-module-files/v/unstable)](https://packagist.org/packages/floor12/yii2-module-files) 7 | [![Total Downloads](https://poser.pugx.org/floor12/yii2-module-files/downloads)](https://packagist.org/packages/floor12/yii2-module-files) 8 | [![License](https://poser.pugx.org/floor12/yii2-module-files/license)](https://packagist.org/packages/floor12/yii2-module-files) 9 | 10 | *Этот файл доступен на [русском языке](README_RU.md).* 11 | 12 | ## About the module 13 | 14 | ![FileInputWidget](https://floor12.net/files/default/get?hash=7ad1b9dee10bb7cb5bd73d1c874724e1) 15 | 16 | This module was designed to solve the problem of creating file fields in ActiveRecord models of the Yii2 framework. The 17 | main components of the module are: 18 | 19 | - `floor12\files\components\FileBehaviour` - behavior that must be connected to the ActiveRecord model; 20 | - `floor12\files\components\FileInputWidget` - an InputWidget that allows you to add, edit and generally work with 21 | files; 22 | - `floor12\files\components\FileListWidget` - an additional widget to display a list of files with the abilities to view 23 | images in the Lightbox2 gallery, download all files of the current field in zip format, and view the Word and Excel 24 | files using the Microsoft office online. 25 | 26 | ### Key features 27 | 28 | - adding one or more fields with files to the ActiveRecord model; 29 | - setting up validation of these fields using the standard `FileValidator` defined in the` rules ()`section; 30 | - in the case of working with images - the ability to configure the image ratio (in this case, when loading an image 31 | through the 32 | `FileInputWidget` widget will automatically open a modal window to crop the image with the desired ratio); 33 | - thumbnails creating with optimal sizes for each case in site template. Also, these thumbnails supports WEBP format; 34 | - download files in ZIP-format 35 | - `FileInputWidget` supports changing of files order by drag-and-drop, cropping and filename updating; 36 | - in case of drag-and-drop uploading, the file result file order is the same as on client folder; 37 | - automatic horizon detection by EXIF ​​tag; 38 | - if you need to add images to the model not with the web interface of the site, but using console parsers and other 39 | similar cases - its possible. For this case, the module includes two classes: `FileCreateFromInstance` 40 | and` FileCreateFromPath` with helps add files to AR model from server file system; 41 | - in case of video files: recoding them to h264 using the ffmpeg utility; 42 | 43 | ### i18n 44 | 45 | At this stage, the module supports the following languages: 46 | 47 | - English 48 | - Russian 49 | 50 | ### Principle of operation 51 | 52 | All files data is stored in the `file` table. The `file` model relay to the model by three fields: 53 | 54 | - `class` - the full class name of the relay model 55 | - `field` - the name of the model field 56 | - `object_id` - primary key of the model 57 | 58 | When file added to the form, it uploads to server in background where all processing takes place. As a result of this 59 | processing, it is written to disk and a new entry is created for it in the `file` table, with the fields` class` and 60 | `field` filled with data from the model, and` object_id` is empty and will assign only after saving the ActiveRecord 61 | model. When a file is deleted from the widget, it is not deleted from the disk and the `file` table, just `obejct_id` 62 | equals to 0. You can use the console command` files / console / clean` to periodically clean up this kind of orphan 63 | files. 64 | 65 | ## Install and setup 66 | 67 | To add this module to your app, just run: 68 | 69 | ```bash 70 | $ composer require floor12/yii2-module-files 71 | ``` 72 | 73 | or add this to the `require` section of your composer.json. 74 | 75 | ```json 76 | "floor12/yii2-module-files": "dev-master" 77 | ``` 78 | 79 | Then execute a migration to create `file` table. 80 | 81 | ```bash 82 | $ ./yii migrate --migrationPath=@vendor/floor12/yii2-module-files/src/migrations/ 83 | ``` 84 | 85 | After that, include module data in `modules` section of application config: 86 | 87 | ```php 88 | 'modules' => [ 89 | 'files' => [ 90 | 'class' => 'floor12\files\Module', 91 | 'storage' => '@app/storage', 92 | 'cache' => '@app/storage_cache', 93 | 'token_salt' => 'some_random_salt', 94 | ], 95 | ], 96 | ... 97 | ``` 98 | 99 | Parameters to set: 100 | 101 | - `storage` - the path alias to the folder to save files and image sources, by default it is located in the `storage` 102 | folder in the project root; 103 | - `cache` - path alias to the folder of thumbnails of images that the module creates on the fly upon request and caches; 104 | - `token_salt` - a unique salt to generate InputWidget tokens. 105 | 106 | ## Usage 107 | 108 | ### Work with ActiveRecord Model 109 | 110 | To add one or more files fields to the ActiveRecord model, you need to connect `floor12\files\components\FileBehaviour` 111 | to it and pass list the field names that will store the files in the model. For example, for the User model, 2 file 112 | fields are defined here 113 | : `avatar` and` documents`: 114 | 115 | ```php 116 | public function behaviors() 117 | { 118 | return [ 119 | 'files' => [ 120 | 'class' => 'floor12\files\components\FileBehaviour', 121 | 'attributes' => [ 122 | 'avatar', 123 | 'documents' 124 | ], 125 | ], 126 | ... 127 | ``` 128 | 129 | To have nice attribute labels in forms, add some labels to `attributeLabels()`: 130 | 131 | ```php 132 | public function attributeLabels() 133 | { 134 | return [ 135 | ... 136 | 'avatar' => 'Аватар', 137 | 'documents' => 'Документы', 138 | ... 139 | ]; 140 | } 141 | ``` 142 | 143 | Setup validation rules in the `rules()` method of ActiveRecord model: 144 | 145 | ```php 146 | public function rules() 147 | { 148 | return [ 149 | // Avatar is required attribute 150 | ['avatar', 'required'], 151 | 152 | // Avatar allow to uploade 1 file with this extensions: jpg, png, jpeg, gif 153 | ['avatar', 'file', 'extensions' => ['jpg', 'png', 'jpeg', 'gif'], 'maxFiles' => 1], 154 | 155 | // Documens allows to upload a few files with this extensions: docx, xlsx 156 | ['documents', 'file', 'extensions' => ['docx', 'xlsx'], 'maxFiles' => 10], 157 | ... 158 | ``` 159 | 160 | ### Work with files 161 | 162 | If `maxFiles` in `FileValidator` equals to 1, this attribute will store an `floor12\files\models\File object`. Example: 163 | 164 | ```php 165 | // The href field contains web path to file source 166 | echo Html::img($model->avatar->href) 167 | 168 | // __toString() method of File object will return href as well 169 | echo Html::img($model->avatar) 170 | ``` 171 | 172 | If the file is image, getPreviewWebPath method returns a web path to image thumbnail. By default thumbnail created with 173 | the jpeg or png format, it depends to source file. But also WEBP option is available. 174 | 175 | `File::getPreviewWebPath(int $width = 0, int $height = 0 ,bool $webp = false)` 176 | 177 | Usage example: 178 | 179 | ```php 180 | // User avatar thumbnail with 200px width 181 | echo Html::img($model->avatar->getPreviewWebPath(200)); 182 | 183 | // User avatar thumbnail with 200px width and WEBP format 184 | echo Html::img($model->avatar->getPreviewWebPath(200, 0, true)); 185 | 186 | ``` 187 | 188 | When `maxFiles` equals to 1, multiple upload is available. In this case, model field will contains an array 189 | if `floor12\files\models \File` objects: 190 | 191 | ```php 192 | foreach ($model->docs as $doc} 193 | Html::a($doc->title, $doc->href); 194 | ``` 195 | 196 | Here is another example, the advanced usage of thumbnails. In this case, we use modern `picture` and` source` tags, as 197 | well as media queries. As a result, we have 8 different thumbnails: 4 has webp format for those browsers that support 198 | this it, and 4 has jpeg format. Devices with retina displays will get an images with double resolution, regular screens 199 | have regular sized pictures. This example also uses different images widths at different screen widths (just as example 200 | of mobile/desktop image switching): 201 | 202 | ```php 203 | 204 | 207 | 210 | 213 | <?= $model->title ?> 218 | 219 | ``` 220 | 221 | ### Picture tag widget 222 | 223 | If object if tyle `File` is image (`$file->isImage() === true`), it can be used with `PictureWidget`. This widget helps 224 | generate html tag with srcset with 2x and webp versions. For example this code: 225 | 226 | ```php 227 | echo PictureWidget::widget([ 228 | 'model' => $file, 229 | 'alt' => 'Some alternative text', 230 | 'width' => 100 231 | ]); 232 | ``` 233 | 234 | will make this html: 235 | 236 | ```html 237 | 238 | 239 | 241 | <?= $model->title ?> 244 | 245 | ``` 246 | 247 | Additional parameters allowed to pass media-queries to widget. 248 | 249 | ### Listing the files 250 | 251 | There is a widget for listing all files. It supports [Lightbox2](https://lokeshdhakar.com/projects/lightbox2/) gallery 252 | to display images and MS Office files preview. Its also supports downloading of the all the files attached to the field 253 | in a ZIP-archive. 254 | 255 | ```php 256 | echo \floor12\files\components\FileListWidget::widget([ 257 | 'files' => $model->docs, 258 | 'downloadAll' => true, 259 | 'zipTitle' => "Documents of {$user->fullname}" 260 | ]) 261 | ``` 262 | 263 | An array of `File` objects must be passed to the widget `files` field. Also additional parameters available: 264 | 265 | - `title` - optionally set the title of the block (by default its taken from `AttributeLabels()`)", 266 | - `downloadAll` - show the "download all" button, 267 | - `zipTitle` - set the file name of zip archive, 268 | - `passFirst` - skip first file in array (it is often necessary to display the gallery without the first picture. For 269 | example, in the news view page, when the first image used to be news main image). 270 | 271 | ![FileInputWidget](https://floor12.net/files/default/get?hash=6482fa93391f5fdcbbf8eb8d242da684) 272 | 273 | ### InputWidget for ActiveFrom 274 | 275 | To display files block in your forms use the `floor12\files\components\FileInputWidget`: 276 | 277 | ```php 278 | field($model, 'avatar')->widget(floor12\files\components\FileInputWidget::class) ?> 279 | ``` 280 | 281 | Moreover, if `maxFiles` parameter in` FileValidator` equals to 1 or more, `FileInputWidget` will take the necessary form 282 | to load one file or several at once. If necessary, you can pass `uploadButtonText` and` uploadButtonClass` parameters to 283 | the widget. 284 | 285 | ## Contributing 286 | 287 | I will be glad of any help in the development, support and bug reporting of this module. -------------------------------------------------------------------------------- /README_RU.md: -------------------------------------------------------------------------------- 1 | # yii2-module-files 2 | 3 | [![Build Status](https://travis-ci.org/floor12/yii2-module-files.svg?branch=master)](https://travis-ci.org/floor12/yii2-module-files) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/floor12/yii2-module-files/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/floor12/yii2-module-files/?branch=master) 5 | [![Latest Stable Version](https://poser.pugx.org/floor12/yii2-module-files/v/stable)](https://packagist.org/packages/floor12/yii2-module-files) 6 | [![Latest Unstable Version](https://poser.pugx.org/floor12/yii2-module-files/v/unstable)](https://packagist.org/packages/floor12/yii2-module-files) 7 | [![Total Downloads](https://poser.pugx.org/floor12/yii2-module-files/downloads)](https://packagist.org/packages/floor12/yii2-module-files) 8 | [![License](https://poser.pugx.org/floor12/yii2-module-files/license)](https://packagist.org/packages/floor12/yii2-module-files) 9 | 10 | ## Информация о модуле 11 | 12 | ![FileInputWidget](https://floor12.net/files/default/get?hash=7ad1b9dee10bb7cb5bd73d1c874724e1) 13 | 14 | Это модуль разработан для того, чтобы решить проблему создание полей с файлами в ActiveRecord моделях фреймворка Yii2. 15 | Основными компонентами модуля являются: 16 | 17 | - `floor12\files\components\FileBehaviour` - поведение, которое необходимо подключить к ActiveRecord модели; 18 | - `floor12\files\components\FileInputWidget` - виджет для формы, позволяющий добавлять, редактировать и в целом работать 19 | с добавленными файлами; 20 | - `floor12\files\components\FileListWidget` - дополнительный виджет для вывода списка файлов с возможностями просмотра 21 | изображений в галереи Lightbox2, загрузке всех файлов текущего поля в формате zip, а так же просмотром Word и Excel 22 | файлов с помощью онлайн офиса от Microsoft. 23 | 24 | ### Основные функции модуля 25 | 26 | - добавление одного и более полей с файлами к ActiveRecord модели; 27 | - настройка валидация этих полей при помощи стандартного `FileValidator`, указанного в секции `rules()`; 28 | - в случае работы с изображениями - возможность сконфигурировать пропорции изображения (в этом случае при загрузке 29 | изображения через `FileInputWidget` виджет автоматически откроет окно для обрезки изображения с нужными пропорциями); 30 | - возможность создания миниатюр для использования в различных местах шаблонов изображений оптимальных размеров. Так же 31 | эти миниатюры могут поддерживают формат WEBP; 32 | - возможность скачивания всех добавленных в одно поле файлов в виде ZIP-архива 33 | - при загрузке изображений через `FileInputWidget` имеется возможность изменять порядок объектов драг-н-дропом, изменять 34 | размер и имя; 35 | - при загрузке файлов драг-н-дропом порядок файлов сохраняется тем же, что и был в момент выделения файлов на 36 | компьютере (это очень удобно, если необходимо добавить к модели, к примеру, 50 изображений в строгом порядке); 37 | - при загрузке изображений автоматическое определение горизонта по EXIF-метке; 38 | - при необходимости добавления изображений к модели не через веб-интерфейс сайта, а при помощь консольных парсеров и 39 | других похожих случаев - такая возможность имеется. Для этого в системе предусмотрено два 40 | класса: `FileCreateFromInstance.php` и `FileCreateFromPath.php`. 41 | - при работе с видео файлами - перекодировка их в h264 при помощи утилиты ffmpeg; 42 | 43 | ### Интернационализация 44 | 45 | На данный этап модуль поддерживает следующие языки: 46 | 47 | - Английский 48 | - Русский 49 | 50 | ### Принцип работы 51 | 52 | Информация о файлах хранится в таблице `file` и содержит связи с моделью через три поля: 53 | 54 | - `class` - полное имя класса связанной модели 55 | - `field` - имя поля модели 56 | - `object_id` - primary key модели 57 | 58 | При работе с виджетом добавления файлов, во время добавления файла в форму происходит его фоновая загрузка и обработка. 59 | В результате этой обработки он записывается на диск и для него создается запись в таблице `file`, где поля `class` 60 | и `field` заполнены данными из модели, а `object_id` присваивается только после сохранения модели ActiveRecord, к 61 | которой подключено поведение. При удалении файла из формы он не удаляется с диска и из таблицы `file`, а просто его 62 | object_id будет обращен в 0. Для периодической очистки такого рода бесхозных файлов можно периодически использоваться 63 | консольную команду `files/console/clean`. 64 | 65 | ## Установка и настройка 66 | 67 | Устанавливаем модуль через composer: 68 | Выполняем команду 69 | 70 | ```bash 71 | $ composer require floor12/yii2-module-files 72 | ``` 73 | 74 | или добавляем в секцию "required" файла composer.json 75 | 76 | ```json 77 | "floor12/yii2-module-files": "dev-master" 78 | ``` 79 | 80 | Далее выполняем миграцию для создания таблицы `file` 81 | 82 | ```bash 83 | $ ./yii migrate --migrationPath=@vendor/floor12/yii2-module-files/src/migrations/ 84 | ``` 85 | 86 | Так как для работы модуля требуются контроллеры, прописываем модуль в конфигурации Yii2 приложения: 87 | 88 | ```php 89 | 'modules' => [ 90 | 'files' => [ 91 | 'class' => 'floor12\files\Module', 92 | 'storage' => '@app/storage', 93 | 'cache' => '@app/storage_cache', 94 | 'token_salt' => 'some_random_salt', 95 | ], 96 | ], 97 | ... 98 | ``` 99 | 100 | Параметры: 101 | 102 | - `storage` - алиас пути к хранилищу файлов и исходников изображений на диске, по умолчанию располагается в папке 103 | storage в корне проекта; 104 | - `cache` - алиас пути к хранилищу миниатюр изображений, которые модуль создает "на лету" по запросу и кеширует; 105 | - `token_salt` - уникальная соль для безопасной работы виджета загрузки файлов. 106 | 107 | ## Использование 108 | 109 | ### Настройка модели ActiveRecord 110 | 111 | Для добавление к модели ActiveRecord одного или нескольких полей с файлами, необходимо подключить к 112 | ней `floor12\files\components\FileBehaviour'` и перечислить названия полей, которые будут хранить файлы. Например, для 113 | модели User здесь будут определены 2 поля для хранения файлов: `avatar` и `documents`: 114 | 115 | ```php 116 | public function behaviors() 117 | { 118 | return [ 119 | 'files' => [ 120 | 'class' => 'floor12\files\components\FileBehaviour', 121 | 'attributes' => [ 122 | 'avatar', 123 | 'documents' 124 | ], 125 | ], 126 | ... 127 | ``` 128 | 129 | Чтобы отобразить красивые названия для полей, прописываем для них лейблы, как будто это обычные поля модели: 130 | 131 | ```php 132 | public function attributeLabels() 133 | { 134 | return [ 135 | ... 136 | 'avatar' => 'Аватар', 137 | 'documents' => 'Документы', 138 | ... 139 | ]; 140 | } 141 | ``` 142 | 143 | В методе `rules()` описываем правила валидации для наших файловых полей: 144 | 145 | ```php 146 | public function rules() 147 | { 148 | return [ 149 | //Аватар является обязательным полем 150 | ['avatar', 'required'], 151 | 152 | //В поле Аватар можно поместить 1 файл с разрешением 'jpg', 'png', 'jpeg', 'gif' 153 | ['avatar', 'file', 'extensions' => ['jpg', 'png', 'jpeg', 'gif'], 'maxFiles' => 1], 154 | 155 | //А в поле documents можно помещать несколько документов в формате MS Word или Excel 156 | ['documents', 'file', 'extensions' => ['docx', 'xlsx'], 'maxFiles' => 10], 157 | ... 158 | ``` 159 | 160 | ### Обращение к файлам 161 | 162 | Если `maxFiles` в `FileValidator` будет равен единице, то поле модели будет хранить экземпляр 163 | класса `floor12\files\models\File`. Например: 164 | 165 | ```php 166 | // Поле href хранит в себе ссылку на исходник файла или картинки 167 | echo Html::img($model->avatar->href) 168 | 169 | //Такая запись равнозначна, так как объект File при приведении к строке возвращает href 170 | echo Html::img($model->avatar) 171 | ``` 172 | 173 | Если файл является изображением, то можно запросить его миниатюру, передав в специальный метод ширину, высоту и флаг 174 | конвертации в WEBP: 175 | 176 | `File::getPreviewWebPath(int $width = 0, int $height = 0 ,bool $webp = false)` 177 | 178 | Пример использования: 179 | 180 | ```php 181 | //Запрашиваем миниатюру аватара пользователя шириной 200 пикселей 182 | echo Html::img($model->avatar->getPreviewWebPath(200)); 183 | 184 | //Запрашиваем миниатюру аватара пользователя шириной 200 пикселей и в формате WEBP 185 | echo Html::img($model->avatar->getPreviewWebPath(200, true)); 186 | 187 | ``` 188 | 189 | В случае, если `maxFiles > 1` и файлов можно загрузить несколько, то поле с файлами будет содержать не экземпляр 190 | объекта `floor12 \files\models\File`, а массив объектов: 191 | 192 | ```php 193 | foreach ($model->docs as $doc} 194 | Html::a($doc->title, $doc->href); 195 | ``` 196 | 197 | Вот еще один пример продвинутого использования миниатюр. В данном варианте мы используем современные теги `picture` 198 | и `source`, а так же медиа-запросы. В результате, у нас имеется 8 миниатюр, 4 в формате webp для тех браузерв, которые 199 | поддерживают этот формат, а 4 в формате jpeg. Кроме того, устройствам с ретиной будут показывать изображения с двойным 200 | разрешением. а обычным экранам - картинки обычного размера. Так же в примере используется разделение на разные 201 | изображения при разной ширине экрана: 202 | 203 | ```php 204 | 205 | 208 | 211 | 214 | <?= $model->title ?> 219 | 220 | ``` 221 | 222 | ### Виджет для тега Picture 223 | 224 | Если объект типа `File` является изображением (`$file->isImage() === true`), то для него можно 225 | использовать `PictureWidget`. Этот виджет поможет в несколько строк кода сгенерировать тег picture с набором source и 226 | srcset на базе заданных параметров. Например: 227 | 228 | ```php 229 | echo PictureWidget::widget([ 230 | 'model' => $file, 231 | 'alt' => 'Some alternative text', 232 | 'width' => 100 233 | ]); 234 | ``` 235 | 236 | сгенерирует следующий html код: 237 | 238 | ```html 239 | 240 | 241 | 243 | <?= $model->title ?> 246 | 247 | ``` 248 | 249 | Дополнительный параметры можно посмотреть в исходном коде виджета. 250 | 251 | ### Виджет для списка файлов 252 | 253 | В поставке модуля, есть виджет для вывода всех файлов, который дает возможность просматривать список файлов конкретного 254 | поля, подключая для его отображения галерею [Lightbox2](https://lokeshdhakar.com/projects/lightbox2/) если в списке есть 255 | картинки, осуществлять предпросмотр файлов MS Office, а так же имеется возможно скачать все приложенные к модели файлы 256 | ZIP-архивом. 257 | 258 | ```php 259 | echo \floor12\files\components\FileListWidget::widget([ 260 | 'files' => $model->docs, 261 | 'downloadAll' => true, 262 | 'zipTitle' => "Документы пользователя {$model->fullname}" 263 | ]) 264 | ``` 265 | 266 | В виджет необходимо передать массив объектов `File`, а так же можно задать дополнительные параметры: 267 | 268 | - `title` - опционально задать заголовок блока (по-умолчанию берется из AttributeLabels)", 269 | - `downloadAll` - показать кнопку "скачать все файлы", 270 | - `zipTitle` - задать название файла для zip-архива, 271 | - `passFirst` - пропустить при выводе первый файл (часто бывает необходимо вывести галерею без первой картинки, 272 | например, в новости, так как первая картинка "ушла" на обложку самой новости - как пример). 273 | 274 | ![FileInputWidget](https://floor12.net/files/default/get?hash=6482fa93391f5fdcbbf8eb8d242da684) 275 | 276 | ### Виджет для ActiveForm 277 | 278 | Во время редактирования модели, необходимо использовать виджет `floor12\files\components\FileInputWidget`: 279 | 280 | ```php 281 | field($model, 'avatar')->widget(floor12\files\components\FileInputWidget::class) ?> 282 | ``` 283 | 284 | При этом, в зависимости того. установлен ли параметр `maxFiles` в `FileValidator` равный единицы или 285 | более, `FileInputWidget` примет необходимый вид, для загрузки одного файла или сразу нескольких. При необходимости можно 286 | передать в виджет текст и для кнопки и класс для кнопки загрузки через параметры `uploadButtonText` 287 | и `uploadButtonClass`. 288 | 289 | ## Участие в разработке 290 | 291 | Буду рад любой помощи в разработке, поддержке и баг-репортах на этот модуль. -------------------------------------------------------------------------------- /assets/yii2-floor12-files-block.css: -------------------------------------------------------------------------------- 1 | div.files-block a, 2 | div.files-block a:hover { 3 | text-decoration: none; } 4 | div.files-block > label { 5 | display: block; 6 | margin-bottom: 2px; } 7 | div.files-block > ul { 8 | display: flex; 9 | flex-wrap: wrap; 10 | padding: 0; 11 | list-style: none; } 12 | div.files-block > ul li { 13 | margin: 0 10px 10px 0; 14 | position: relative; } 15 | div.files-block > ul li a.f12-file-object { 16 | margin: 0 10px 10px 0; 17 | width: 70px; 18 | height: 70px; 19 | overflow: hidden; 20 | position: relative; 21 | border-radius: 3px; 22 | display: flex; 23 | background-size: cover; 24 | flex-direction: column; 25 | background-color: #e9e9e9; 26 | align-items: center; } 27 | div.files-block > ul li a.f12-file-object svg { 28 | width: 50px; 29 | height: 38px !important; 30 | min-height: 38px; 31 | margin: 5px 0 0 0; } 32 | div.files-block > ul li a.f12-file-object span { 33 | overflow: hidden; 34 | text-overflow: ellipsis; 35 | font-size: 8px; 36 | margin: 3px 5px; 37 | text-align: center; 38 | overflow-wrap: break-word; 39 | max-width: 65px; } 40 | div.files-block > ul li a.files-download-btn { 41 | position: absolute; 42 | top: 0; 43 | right: 10px; 44 | padding: 1px 5px; 45 | background: #ffffffc4; 46 | z-index: 10; 47 | border-bottom-left-radius: 3px; } 48 | div.files-block > ul li a.f12-files-btn-download-all { 49 | border: 0; 50 | background: #e9e9e9; 51 | padding: 4px 10px; 52 | border-radius: 2px; 53 | margin: 30px 0 0 0; 54 | display: block; } 55 | 56 | /*# sourceMappingURL=yii2-floor12-files-block.css.map */ 57 | -------------------------------------------------------------------------------- /assets/yii2-floor12-files-block.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AACE;uBACQ;EACN,eAAe,EAAE,IAAI;AAGvB,uBAAQ;EACN,OAAO,EAAE,KAAK;EACd,aAAa,EAAE,GAAG;AAGpB,oBAAK;EACH,OAAO,EAAE,IAAI;EACb,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,CAAC;EACV,UAAU,EAAE,IAAI;EAEhB,uBAAG;IACD,MAAM,EAAE,aAAa;IACrB,QAAQ,EAAE,QAAQ;IAElB,yCAAkB;MAChB,MAAM,EAAE,aAAa;MACrB,KAAK,EAAE,IAAI;MACX,MAAM,EAAE,IAAI;MACZ,QAAQ,EAAE,MAAM;MAChB,QAAQ,EAAE,QAAQ;MAClB,aAAa,EAAE,GAAG;MAClB,OAAO,EAAE,IAAI;MACb,eAAe,EAAE,KAAK;MACtB,cAAc,EAAE,MAAM;MACtB,gBAAgB,EAAE,OAAO;MACzB,WAAW,EAAE,MAAM;MAEnB,6CAAI;QACF,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,eAAe;QACvB,UAAU,EAAE,IAAI;QAChB,MAAM,EAAE,SAAS;MAGnB,8CAAK;QACH,QAAQ,EAAE,MAAM;QAChB,aAAa,EAAE,QAAQ;QACvB,SAAS,EAAE,GAAG;QACd,MAAM,EAAE,OAAO;QACf,UAAU,EAAE,MAAM;QAClB,aAAa,EAAE,UAAU;QACzB,SAAS,EAAE,IAAI;IAInB,4CAAqB;MACnB,QAAQ,EAAE,QAAQ;MAClB,GAAG,EAAE,CAAC;MACN,KAAK,EAAE,IAAI;MACX,OAAO,EAAE,OAAO;MAChB,UAAU,EAAE,SAAS;MACrB,OAAO,EAAE,EAAE;MACX,yBAAyB,EAAE,GAAG;IAGhC,oDAA6B;MAC3B,MAAM,EAAE,CAAC;MACT,UAAU,EAAE,OAAO;MACnB,OAAO,EAAE,QAAQ;MACjB,aAAa,EAAE,GAAG;MAClB,MAAM,EAAE,UAAU;MAClB,OAAO,EAAE,KAAK", 4 | "sources": ["yii2-floor12-files-block.scss"], 5 | "names": [], 6 | "file": "yii2-floor12-files-block.css" 7 | } 8 | -------------------------------------------------------------------------------- /assets/yii2-floor12-files-block.js: -------------------------------------------------------------------------------- 1 | function filesDownloadAll(title, event, yiiDownloadAllLink) { 2 | obj = $(event.target).parents('div.files-block'); 3 | hashes = ""; 4 | $.each(obj.find('.f12-file-object'), function (key, val) { 5 | hashes += "&hash[]=" + $(val).data('hash'); 6 | }); 7 | console.log(hashes); 8 | 9 | window.open(yiiDownloadAllLink + "?title=" + title + hashes); 10 | 11 | } -------------------------------------------------------------------------------- /assets/yii2-floor12-files-block.scss: -------------------------------------------------------------------------------- 1 | div.files-block { 2 | a, 3 | a:hover { 4 | text-decoration: none; 5 | } 6 | 7 | > label { 8 | display: block; 9 | margin-bottom: 2px; 10 | } 11 | 12 | > ul { 13 | display: flex; 14 | flex-wrap: wrap; 15 | padding: 0; 16 | list-style: none; 17 | 18 | li { 19 | margin: 0 10px 10px 0; 20 | position: relative; 21 | 22 | a.f12-file-object { 23 | margin: 0 10px 10px 0; 24 | width: 70px; 25 | height: 70px; 26 | overflow: hidden; 27 | position: relative; 28 | border-radius: 3px; 29 | display: flex; 30 | background-size: cover; 31 | flex-direction: column; 32 | background-color: #e9e9e9; 33 | align-items: center; 34 | 35 | svg { 36 | width: 50px; 37 | height: 38px !important; 38 | min-height: 38px; 39 | margin: 5px 0 0 0; 40 | } 41 | 42 | span { 43 | overflow: hidden; 44 | text-overflow: ellipsis; 45 | font-size: 8px; 46 | margin: 3px 5px; 47 | text-align: center; 48 | overflow-wrap: break-word; 49 | max-width: 65px; 50 | } 51 | } 52 | 53 | a.files-download-btn { 54 | position: absolute; 55 | top: 0; 56 | right: 10px; 57 | padding: 1px 5px; 58 | background: #ffffffc4; 59 | z-index: 10; 60 | border-bottom-left-radius: 3px; 61 | } 62 | 63 | a.f12-files-btn-download-all { 64 | border: 0; 65 | background: #e9e9e9; 66 | padding: 4px 10px; 67 | border-radius: 2px; 68 | margin: 30px 0 0 0; 69 | display: block; 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /assets/yii2-floor12-files.css: -------------------------------------------------------------------------------- 1 | 2 | div.floor12-files-widget-block { 3 | background-color: #fcfcfc; 4 | padding: 6px 4px 0 7px; 5 | border: 1px solid #e3e3e3; 6 | border-radius: 4px; 7 | transition: all 0.3s; 8 | margin: -2px -5px; 9 | } 10 | 11 | div.floor12-files-widget-block button.btn-upload { 12 | float: left; 13 | height: 60px; 14 | margin: 0 10px 8px 0; 15 | transition: all 0.3s; 16 | width: 80px; 17 | background-color: #c0c0c0; 18 | color: #ffffff; 19 | background: linear-gradient(45deg, rgb(151, 151, 151) 0%, rgb(184, 184, 184) 92%); 20 | border: none; 21 | } 22 | 23 | div.floor12-files-widget-single-block button.btn-upload { 24 | transition: all 0.3s; 25 | color: #ffffff; 26 | background: #c0c0c0 linear-gradient(45deg, rgb(151, 151, 151) 0%, rgb(184, 184, 184) 92%); 27 | border: none; 28 | padding: 10px; 29 | border-radius: 2px; 30 | } 31 | 32 | div.floor12-files-widget-block button.btn-upload div.icon { 33 | display: block; 34 | font-size: 20px; 35 | } 36 | 37 | div.floor12-files-widget-block-drug-over { 38 | background-color: #fbfffd; 39 | border: 1px #357025 dotted; 40 | 41 | } 42 | 43 | div.floor12-file-object { 44 | width: 80px; 45 | height: 60px; 46 | margin: 0 10px 8px 0; 47 | background-color: #ebebeb; 48 | border-radius: 4px; 49 | float: left; 50 | text-align: center; 51 | font-size: 60%; 52 | cursor: pointer; 53 | /* transition: all 0.2s; */ 54 | text-overflow: ellipsis; 55 | overflow: hidden; 56 | font-weight: 600; 57 | white-space: nowrap; 58 | padding: 0 4px; 59 | position: relative; 60 | } 61 | 62 | div.floor12-file-object .floor12-file-percents { 63 | position: absolute; 64 | width: 90%; 65 | text-align: center; 66 | top: 22px; 67 | font-size: 120%; 68 | color: #666; 69 | font-weight: bold; 70 | } 71 | 72 | div.floor12-file-object svg.progress-circle { 73 | transform: rotate(-90deg); 74 | } 75 | 76 | div.floor12-file-object svg circle { 77 | transition: stroke-dashoffset 0.5s linear; 78 | } 79 | 80 | div.floor12-file-object div.file-icon { 81 | font-size: 360%; 82 | display: block; 83 | margin: 0; 84 | color: #787878; 85 | } 86 | 87 | div.floor12-file-object div.file-icon svg { 88 | width: 1em; 89 | height: 1em; 90 | margin: 6px 0 0 0; 91 | vertical-align: -.125em; 92 | } 93 | 94 | 95 | div.floor12-file-object-image { 96 | background-size: cover; 97 | background-position: center; 98 | border: 1px #e1e1e1 solid; 99 | } 100 | 101 | div.floor12-file-object-uploading { 102 | background-position: center; 103 | background-repeat: no-repeat; 104 | font-size: 130%; 105 | text-align: center; 106 | line-height: 62px; 107 | width: 74px; 108 | position: absolute; 109 | height: 59px; 110 | margin-top: 3px; 111 | } 112 | 113 | div.floor12-file-object-bar { 114 | width: 100%; 115 | background: #b0b0b0; 116 | height: 6px; 117 | border-radius: 2px; 118 | } 119 | 120 | ul.dropdown-menu-file-object-multi { 121 | margin-top: -20px; 122 | margin-left: -41px; 123 | } 124 | 125 | ul.dropdown-menu-file-object-single { 126 | top: 0; 127 | } 128 | 129 | ul.dropdown-menu-file-object > li > a { 130 | padding: 3px 10px; 131 | } 132 | 133 | ul.dropdown-menu-file-object > li > a > svg { 134 | width: 18px; 135 | } 136 | 137 | div.floor12-file-object:hover { 138 | box-shadow: 1px 1px 16px #00000063; 139 | } 140 | 141 | div.floor12-file-object:hover a.btn-file-object-delete { 142 | opacity: 1; 143 | } 144 | 145 | div.floor12-files-widget-single-block div.files-btn-group { 146 | width: 100%; 147 | } 148 | 149 | div.floor12-single-file-ratio { 150 | width: 100%; 151 | height: 0; 152 | padding-bottom: 70%; 153 | border-radius: 3px; 154 | } 155 | 156 | div#cropperModal { 157 | z-index: 999999999; 158 | } 159 | 160 | #cropperArea { 161 | width: 100%; 162 | } 163 | 164 | #cropperArea img { 165 | width: 100%; 166 | max-height: 600px; 167 | max-width: 100%; 168 | } 169 | 170 | #yii2-file-title-editor { 171 | position: absolute; 172 | width: 100%; 173 | max-width: 420px; 174 | border-radius: 3px; 175 | box-shadow: 0 1px 4px #00000024; 176 | display: none; 177 | z-index: 999999999; 178 | } 179 | 180 | #yii2-file-alt-editor { 181 | position: absolute; 182 | width: 100%; 183 | max-width: 420px; 184 | border-radius: 3px; 185 | box-shadow: 0 1px 4px #00000024; 186 | display: none; 187 | z-index: 999999999; 188 | } 189 | 190 | .input-group-btn .btn { 191 | padding-bottom: 7px; 192 | } 193 | 194 | div.floor12-single-file-object-no-image { 195 | padding: 5px 12px; 196 | border-radius: 2px; 197 | transition: all 0.3s; 198 | background-color: #c0c0c0; 199 | color: #ffffff; 200 | background: linear-gradient(45deg, rgb(151, 151, 151) 0%, rgb(184, 184, 184) 92%); 201 | border: none; 202 | } 203 | 204 | div.floor12-files-widget-list-multi { 205 | min-height: 68px; 206 | } 207 | 208 | /*progress*/ 209 | 210 | -------------------------------------------------------------------------------- /assets/yii2-floor12-files.js: -------------------------------------------------------------------------------- 1 | console.log('Yii2 files model init.'); 2 | 3 | 4 | var currentCroppingImageId; 5 | var currentRenamingFileId; 6 | var cropper; 7 | var removeFileOnCropCancel; 8 | var yii2CropperRoute; 9 | var yii2UploadRoute; 10 | var uploaderSettings = {}; 11 | 12 | $(document).on('change', '.yii2-files-upload-field', function () { 13 | 14 | obj = $(this); 15 | 16 | var formData = new FormData(); 17 | formData.append('file', obj[0].files[0]); 18 | formData.append('modelClass', obj.data('modelclass')); 19 | formData.append('attribute', obj.data('attribute')); 20 | formData.append('mode', obj.data('mode')); 21 | formData.append('ratio', obj.data('ratio')); 22 | formData.append('name', obj.data('name')); 23 | formData.append('count', 1); 24 | formData.append('_fileFormToken', yii2FileFormToken); 25 | 26 | 27 | $.ajax({ 28 | url: yii2UploadRoute, 29 | type: 'POST', 30 | data: formData, 31 | processData: false, // tell jQuery not to process the data 32 | contentType: false, // tell jQuery not to set contentType 33 | success: function (response) { 34 | id = '#files-widget-block_' + obj.data('block'); 35 | $(response).appendTo(id).find('div.floor12-files-widget-list'); 36 | } 37 | }); 38 | }); 39 | 40 | 41 | function clipboard(text) { 42 | //based on https://stackoverflow.com/a/12693636 43 | document.oncopy = function (event) { 44 | event.clipboardData.setData("Text", text); 45 | event.preventDefault(); 46 | }; 47 | document.execCommand("Copy"); 48 | document.oncopy = undefined; 49 | f12notification.info(text, 1); 50 | } 51 | 52 | function updateProgressCircle(val, btnGroup) { 53 | result = 169.646 * (1 - val / 100); 54 | btnGroup.querySelector('svg #progress-circle').setAttribute('stroke-dashoffset', result); 55 | btnGroup.querySelector('.floor12-file-percents').innerHTML = val + '%'; 56 | // setAttribute('stroke-dashoffset', result); 57 | } 58 | 59 | var observer = new MutationObserver(function (mutations) { 60 | percent = mutations[0].target.style.width.replace('%', ''); 61 | btnGroup = mutations[0].target.parentElement.parentElement; 62 | updateProgressCircle(percent, btnGroup); 63 | }); 64 | 65 | var lastUploader = null; 66 | 67 | function Yii2FilesUploaderSet(id, className, attribute, scenario, name) { 68 | 69 | var mode = 'multi'; 70 | var blockName = "#" + id; 71 | var block = $(blockName); 72 | var uploadButton = block.find('button.btn-upload')[0]; 73 | var filesList = block.find('.floor12-files-widget-list')[0]; 74 | var ratio = 0; 75 | 76 | var csrf = block.parents('form').find('input[name=' + yii2CsrfParam + ']').val(); 77 | 78 | if (block.data('ratio')) 79 | ratio = block.data('ratio'); 80 | 81 | if (block.hasClass('floor12-files-widget-single-block')) { 82 | mode = 'single'; 83 | toggleSingleUploadButton(block); 84 | } 85 | 86 | uploaderSettings[id] = { 87 | modelClass: className, 88 | attribute: attribute, 89 | scenario: scenario, 90 | mode: mode, 91 | name: name, 92 | ratio: ratio, 93 | count: block.find('.floor12-files-widget-list .floor12-file-object').length, 94 | _fileFormToken: yii2FileFormToken 95 | } 96 | 97 | uploaderSettings[id][yii2CsrfParam] = csrf 98 | console.log(uploaderSettings[id]); 99 | var uploader = new ss.SimpleUpload({ 100 | button: uploadButton, 101 | url: yii2UploadRoute, 102 | name: 'file', 103 | dropzone: block, 104 | dragClass: 'floor12-files-widget-block-drug-over', 105 | multiple: true, 106 | multipleSelect: true, 107 | data: uploaderSettings[id], 108 | onSubmit: function (filename, extension, uploadBtn, size) { 109 | uploaderSettings[id].count++; 110 | var svg = '\t\n' + 111 | '\t\t\n' + 112 | '\t\t\n' + 113 | '\t'; 114 | 115 | var fileId = generateId(filename); 116 | var btnGroup = document.createElement('div'); 117 | var fileObject = document.createElement('div'); 118 | var bar = document.createElement('div'); 119 | var percents = document.createElement('div'); 120 | btnGroup.setAttribute('id', fileId); 121 | btnGroup.className = 'btn-group files-btn-group'; 122 | fileObject.className = 'floor12-file-object'; 123 | percents.className = 'floor12-file-percents'; 124 | this.setProgressBar(bar); 125 | 126 | fileObject.innerHTML = svg; 127 | 128 | observer.observe(bar, { 129 | attributes: true 130 | }); 131 | 132 | fileObject.appendChild(bar); 133 | fileObject.appendChild(percents); 134 | btnGroup.appendChild(fileObject); 135 | 136 | if (mode == 'single') { 137 | $(filesList).html(''); 138 | } 139 | $(filesList).append(btnGroup); 140 | }, 141 | onComplete: function (filename, response) { 142 | if (!response) { 143 | console.log(filename + 'upload failed'); 144 | uploaderSettings[id].count--; 145 | return false; 146 | } 147 | f12notification.info(FileUploadedText, 1); 148 | idName = "#" + generateId(filename); 149 | $(idName).replaceWith($(response)); 150 | if (mode == 'single') 151 | toggleSingleUploadButton(block); 152 | }, 153 | onError: function (filename, errorType, status, statusText, response, uploadBtn, fileSize) { 154 | console.log(uploaderSettings[id]); 155 | uploaderSettings[id].count--; 156 | data = { 157 | responseText: response, 158 | status: status, 159 | statusText: statusText, 160 | }; 161 | processError(data); 162 | idName = "#" + generateId(filename); 163 | $(idName).remove(); 164 | } 165 | 166 | // progressUrl: 'uploadProgress.php', // enables cross-browser progress support (more info below) 167 | // responseType: 'json', 168 | // allowedExtensions: ['jpg', 'jpeg', 'png', 'gif'], 169 | // maxSize: 1024, // kilobytes 170 | // hoverClass: 'ui-state-hover', 171 | // focusClass: 'ui-state-focus', 172 | // disabledClass: 'ui-state-disabled', 173 | }); 174 | lastUploader = uploader; 175 | } 176 | 177 | function generateId(filename) { 178 | return 'id-' + filename.replace(/[^0-9a-zA-Z]/g, ""); 179 | } 180 | 181 | function showUploadButton(event) { 182 | obj = $(event.target); 183 | obj.parents('div.floor12-files-widget-single-block').find('button').show(); 184 | } 185 | 186 | function toggleSingleUploadButton(block) { 187 | if (block.find('div.floor12-single-file-object').length > 0) 188 | block.find('button').hide(); 189 | else 190 | block.find('button').show(); 191 | } 192 | 193 | function sortableFiles() { 194 | $(".floor12-files-widget-list-multi").sortable({ 195 | opacity: 0.5, 196 | revert: 1, 197 | items: "div.files-btn-group", 198 | connectWith: ".floor12-files-widget-list-multi" 199 | }); 200 | } 201 | 202 | function removeFile(id) { 203 | id = "#yii2-file-object-" + id; 204 | const blockId = $(id).parents('.files-widget-block').attr('id'); 205 | 206 | $(id).parents('div.files-btn-group').fadeOut(200, function () { 207 | $(this).remove(); 208 | f12notification.info(FileRemovedText, 1); 209 | }); 210 | 211 | uploaderSettings[blockId].count--; 212 | return false; 213 | } 214 | 215 | function removeAllFiles(event) { 216 | console.log('removeAllFiles'); 217 | $(event.target).parents('div.floor12-files-widget-list').find('div.files-btn-group').fadeOut(200, function () { 218 | $(this).remove(); 219 | }); 220 | const blockId = $(event.target).parents('.floor12-files-widget-block').attr('id'); 221 | uploaderSettings[blockId].count = 0; 222 | f12notification.info(FilesRemovedText, 1); 223 | return false; 224 | } 225 | 226 | function initCropperLayout() { 227 | if (yii2CropperRoute.length > 0) 228 | $.get(yii2CropperRoute, function (response) { 229 | $('body').append(response); 230 | 231 | $('#yii2-file-title-editor input').on('keyup', function (e) { 232 | if (e.keyCode == 13) { 233 | saveFileTitle() 234 | } 235 | }); 236 | }) 237 | } 238 | 239 | function initCropper(id, url, ratio, remove) { 240 | $('#cropperModal').modal({keyboard: false, backdrop: 'static'}); 241 | 242 | currentCroppingImageId = id; 243 | 244 | removeFileOnCropCancel = false; 245 | if (remove) 246 | removeFileOnCropCancel = true; 247 | 248 | currentCropImage = $('').attr('src', url); 249 | $('#cropperArea').html(""); 250 | $('#cropperArea').append(currentCropImage); 251 | 252 | autoCrop = false; 253 | aspectRatio = NaN; 254 | $('#cropper-btn-cancel').show(); 255 | 256 | console.log(cropperHideCancel); 257 | if (cropperHideCancel == 'true') { 258 | $('#cropper-btn-cancel').hide(); 259 | } 260 | 261 | 262 | if (ratio) { 263 | autoCrop = true; 264 | aspectRatio = ratio; 265 | $('.cropper-ratio-btn-group').hide(); 266 | } 267 | 268 | setTimeout(function () { 269 | cropper = currentCropImage.cropper({ 270 | viewMode: 1, 271 | background: false, 272 | zoomable: false, 273 | autoCrop: autoCrop, 274 | aspectRatio: aspectRatio, 275 | }); 276 | }, 1000) 277 | } 278 | 279 | function stopCrop(id) { 280 | $('#cropperModal').modal('hide'); 281 | if (removeFileOnCropCancel) 282 | removeFile(currentCroppingImageId); 283 | } 284 | 285 | function cropImage() { 286 | сropBoxData = cropper.cropper('getCropBoxData'); 287 | imageData = cropper.cropper('getImageData'); 288 | canvasData = cropper.cropper('getCanvasData'); 289 | ratio = imageData.height / imageData.naturalHeight; 290 | cropLeft = (сropBoxData.left - canvasData.left) / ratio; 291 | cropTop = (сropBoxData.top - canvasData.top) / ratio; 292 | cropWidth = сropBoxData.width / ratio; 293 | cropHeight = сropBoxData.height / ratio; 294 | rotated = imageData.rotate; 295 | 296 | data = { 297 | id: currentCroppingImageId, 298 | width: cropWidth, 299 | height: cropHeight, 300 | top: cropTop, 301 | left: cropLeft, 302 | rotated: rotated, 303 | _fileFormToken: yii2FileFormToken 304 | }; 305 | 306 | removeFileOnCropCancel = false; 307 | 308 | $.ajax({ 309 | url: yii2CropRoute, 310 | 'method': 'POST', 311 | data: data, 312 | success: function (response) { 313 | id = '#yii2-file-object-' + currentCroppingImageId; 314 | if ($(id).find('img').length) 315 | $(id).find('img').attr('src', response); 316 | else { 317 | $(id).css('background-image', 'none'); 318 | $(id).css('background-image', 'url(' + response + ')'); 319 | } 320 | stopCrop(); 321 | f12notification.info(FileSavedText, 1); 322 | }, 323 | error: function (response) { 324 | processError(response); 325 | } 326 | }) 327 | } 328 | 329 | function showRenameFileForm(id, event) { 330 | var blockId = '#yii2-file-object-' + id; 331 | var title = $(blockId).attr('title'); 332 | currentRenamingFileId = id; 333 | $('#yii2-file-title-editor').css('top', event.clientY).css('left', event.clientX - 70).fadeIn(100); 334 | $('#yii2-file-title-editor input').val(title).focus(); 335 | } 336 | 337 | function showAltForm(id, event) { 338 | var blockId = '#yii2-file-object-' + id; 339 | var alt = $(blockId).data('alt'); 340 | console.log(alt); 341 | currentRenamingFileId = id; 342 | $('#yii2-file-alt-editor').css('top', event.clientY).css('left', event.clientX - 70).fadeIn(100); 343 | $('#yii2-file-alt-editor input').val(alt).focus(); 344 | } 345 | 346 | function hideYii2FileTitleEditor() { 347 | $('#yii2-file-title-editor').fadeOut(100); 348 | currentRenamingFileId = null; 349 | } 350 | 351 | function hideYii2FileAltEditor() { 352 | $('#yii2-file-alt-editor').fadeOut(100); 353 | currentRenamingFileId = null; 354 | } 355 | 356 | function saveFileTitle() { 357 | $('#yii2-file-title-editor').fadeOut(100); 358 | val = $('#yii2-file-title-editor input').val(); 359 | blockId = '#yii2-file-object-' + currentRenamingFileId; 360 | $(blockId).attr('title', val); 361 | $(blockId).attr('data-title', val); 362 | 363 | $.ajax({ 364 | url: yii2RenameRoute, 365 | method: 'POST', 366 | data: {id: currentRenamingFileId, title: val, _fileFormToken: yii2FileFormToken}, 367 | success: function () { 368 | f12notification.info(FileRenamedText, 1); 369 | }, 370 | error: function (response) { 371 | processError(response); 372 | } 373 | } 374 | ); 375 | currentRenamingFileId = null; 376 | } 377 | 378 | function saveFileAlt() { 379 | $('#yii2-file-alt-editor').fadeOut(100); 380 | val = $('#yii2-file-alt-editor input').val(); 381 | blockId = '#yii2-file-object-' + currentRenamingFileId; 382 | $(blockId).data('alt', val) 383 | 384 | $.ajax({ 385 | url: yii2AltRoute, 386 | method: 'POST', 387 | data: {id: currentRenamingFileId, alt: val, _fileFormToken: yii2FileFormToken}, 388 | success: function () { 389 | f12notification.info(FileRenamedText, 1); 390 | }, 391 | error: function (response) { 392 | processError(response); 393 | } 394 | } 395 | ); 396 | currentRenamingFileId = null; 397 | } 398 | 399 | $(document).ready(function () { 400 | setInterval(function () { 401 | sortableFiles() 402 | }, 2000); 403 | 404 | sortableFiles(); 405 | 406 | initCropperLayout(); 407 | 408 | }); 409 | 410 | 411 | -------------------------------------------------------------------------------- /assets/yii2-floor12-lightbox-params.js: -------------------------------------------------------------------------------- 1 | lightbox.option({ 2 | 'resizeDuration': 50, 3 | 'fadeDuration': 50, 4 | 'imageFadeDuration': 50, 5 | }); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "floor12/yii2-module-files", 3 | "description": "Yii2 module to upload and manage files to your models.", 4 | "type": "yii2-extension", 5 | "minimum-stability": "stable", 6 | "keywords": [ 7 | "floor12", 8 | "yii2 module", 9 | "files", 10 | "upload", 11 | "yii", 12 | "yii2", 13 | "yii 2" 14 | ], 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "floor12", 19 | "email": "floor12@floor12.net", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": ">=7.3", 25 | "yiisoft/yii2": "^2.0.", 26 | "ext-gd": "*", 27 | "ext-zip": "*", 28 | "ext-exif": "*", 29 | "yiisoft/yii2-jui": "*", 30 | "yii2mod/yii2-enum": "^1.7", 31 | "floor12/yii2-notification": "*", 32 | "bower-asset/cropper": "*", 33 | "bower-asset/lightbox2": "*", 34 | "bower-asset/simple-ajax-uploader": ">=2.6.7", 35 | "ext-fileinfo": "*" 36 | }, 37 | "require-dev": { 38 | "phpunit/phpunit": ">=7.0" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "floor12\\files\\": "src" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "floor12\\files\\tests\\": "tests" 48 | } 49 | }, 50 | "repositories": [ 51 | { 52 | "type": "composer", 53 | "url": "https://asset-packagist.org" 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests 11 | 12 | 13 | 14 | 15 | ./src/ 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | 'db']; 66 | /** 67 | * @var Connection 68 | */ 69 | public $db; 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | public function init() 75 | { 76 | $this->registerTranslations(); 77 | $this->db = Yii::$app->{$this->params['db']}; 78 | $this->storageFullPath = Yii::getAlias($this->storage); 79 | $this->cacheFullPath = Yii::getAlias($this->cache); 80 | } 81 | 82 | /** 83 | * @return void 84 | */ 85 | public function registerTranslations() 86 | { 87 | $i18n = Yii::$app->i18n; 88 | $i18n->translations['files'] = [ 89 | 'class' => 'yii\i18n\PhpMessageSource', 90 | 'sourceLanguage' => 'en-US', 91 | 'basePath' => '@vendor/floor12/yii2-module-files/src/messages', 92 | ]; 93 | } 94 | 95 | } -------------------------------------------------------------------------------- /src/actions/GetFileAction.php: -------------------------------------------------------------------------------- 1 | $hash]); 17 | 18 | if (!$model) 19 | throw new NotFoundHttpException("Запрашиваемый файл не найден"); 20 | 21 | if (!file_exists($model->rootPath)) 22 | throw new NotFoundHttpException('Запрашиваемый файл не найден на диске.'); 23 | 24 | Yii::$app->response->headers->set('Last-Modified', date("c", $model->created)); 25 | Yii::$app->response->headers->set('Cache-Control', 'public, max-age=' . (60 * 60 * 24 * 15)); 26 | 27 | if ($model->type == FileType::IMAGE && $model->watermark) { 28 | $image = new SimpleImage(); 29 | $image->load($model->rootPath); 30 | $image->watermark($model->watermark); 31 | $res = $image->output(IMAGETYPE_JPEG); 32 | Yii::$app->response->sendContentAsFile($res, $model->title, ['inline' => true, 'mimeType' => "image/jpeg", 'filesize' => $model->size]); 33 | } else { 34 | $stream = fopen($model->rootPath, 'rb'); 35 | Yii::$app->response->sendStreamAsFile($stream, $model->title, ['inline' => true, 'mimeType' => $model->content_type, 'filesize' => $model->size]); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/actions/GetPreviewAction.php: -------------------------------------------------------------------------------- 1 | loadAndCheckModel($hash); 35 | $this->width = $width; 36 | 37 | if ($width && 38 | $this->model->content_type !== 'image/svg+xml' && 39 | $this->model->content_type !== 'image/svg') { 40 | $this->sendPreview($width, $webp); 41 | } else { 42 | $this->sendAsIs(); 43 | } 44 | } 45 | 46 | /** 47 | * @param string $hash 48 | * @throws NotFoundHttpException 49 | */ 50 | private function loadAndCheckModel(string $hash): void 51 | { 52 | $this->model = File::findOne(['hash' => $hash]); 53 | if (!$this->model) 54 | throw new NotFoundHttpException("Запрашиваемый файл не найден"); 55 | 56 | if (!$this->model->isImage() && !$this->model->isVideo()) 57 | throw new NotFoundHttpException("Запрашиваемый файл не является изображением"); 58 | 59 | if (!file_exists($this->model->rootPath)) 60 | throw new NotFoundHttpException('Запрашиваемый файл не найден на диске.'); 61 | } 62 | 63 | /** 64 | * @param $width 65 | * @throws InvalidConfigException 66 | * @throws NotFoundHttpException 67 | * @throws RangeNotSatisfiableHttpException 68 | */ 69 | protected function sendPreview($width, $webp) 70 | { 71 | $filename = Yii::createObject(ImagePreviewer::class, [$this->model, $width, $webp])->getUrl(); 72 | 73 | if (!file_exists($filename)) 74 | throw new NotFoundHttpException('Запрашиваемый файл не найден на диске.'); 75 | 76 | $response = Yii::$app->response; 77 | $response->format = Response::FORMAT_RAW; 78 | $coontentType = mime_content_type($filename); 79 | $this->setHeaders($response, $coontentType, md5($this->model->created)); 80 | $stream = fopen($filename, 'rb'); 81 | Yii::$app->response->sendStreamAsFile($stream, $this->model->title, [ 82 | 'inline' => true, 83 | 'mimeType' => $this->model->content_type, 84 | 'filesize' => $this->model->size 85 | ]); 86 | } 87 | 88 | /** 89 | * @param $response 90 | * @param string $contentType 91 | * @param string $etag 92 | */ 93 | private function setHeaders($response, string $contentType, string $etag): void 94 | { 95 | $response->headers->set('Last-Modified', date("c", $this->model->created)); 96 | $response->headers->set('Cache-Control', 'public, max-age=' . self::HEADER_CACHE_TIME); 97 | $response->headers->set('Content-Type', $contentType . '; charset=utf-8'); 98 | $response->headers->set('ETag', $etag); 99 | } 100 | 101 | /** 102 | * @throws RangeNotSatisfiableHttpException 103 | */ 104 | protected function sendAsIs() 105 | { 106 | $stream = fopen($this->model->rootPath, 'rb'); 107 | Yii::$app->response->sendStreamAsFile($stream, $this->model->title, [ 108 | 'inline' => true, 109 | 'mimeType' => $this->model->content_type, 110 | 'filesize' => $this->model->size 111 | ]); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/assets/CropperAsset.php: -------------------------------------------------------------------------------- 1 | '; 15 | const TRASH = ''; 16 | const PLUS = ''; 17 | const CROP = ''; 18 | const RENAME = ''; 19 | const VIEW = ''; 20 | const DOWNLOAD = ''; 21 | const EXCLAMATION = ''; 22 | const LINK = ''; 23 | 24 | const FILE = ''; 25 | const FILE_WORD = ''; 26 | const FILE_EXCEL = ''; 27 | const FILE_POWERPOINT = ''; 28 | const FILE_ARCHIVE = ''; 29 | const FILE_AUDIO = ''; 30 | const FILE_PDF = ''; 31 | const FILE_VIDEO = ''; 32 | 33 | const SAVE = ''; 34 | const CLOSE = ''; 35 | const ARROW_LEFT = ''; 36 | const ARROW_RIGHT = ''; 37 | const ROTATE_LEFT = ''; 38 | const ROTATE_RIGHT = ''; 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/assets/LightboxAsset.php: -------------------------------------------------------------------------------- 1 | 'filesSave', 40 | ActiveRecord::EVENT_AFTER_UPDATE => 'filesSave', 41 | ActiveRecord::EVENT_AFTER_DELETE => 'filesDelete', 42 | ActiveRecord::EVENT_BEFORE_VALIDATE => 'validateRequiredFields' 43 | ]; 44 | } 45 | 46 | protected $cachedFiles = []; 47 | 48 | 49 | /** 50 | * Метод сохранения в базу связей с файлами. Вызывается после сохранения основной модели AR. 51 | * @throws ErrorException 52 | * @throws \yii\db\Exception 53 | */ 54 | 55 | public function filesSave() 56 | { 57 | $order = 0; 58 | if ($this->_values) { 59 | 60 | foreach ($this->_values as $field => $ids) { 61 | 62 | Yii::$app->db->createCommand()->update( 63 | "{{%file}}", 64 | ['object_id' => 0], 65 | [ 66 | 'class' => $this->owner->className(), 67 | 'object_id' => $this->owner->id, 68 | 'field' => $field, 69 | ] 70 | )->execute(); 71 | 72 | if ($ids) foreach ($ids as $id) { 73 | if (empty($id)) 74 | continue; 75 | $file = File::findOne($id); 76 | if ($file) { 77 | $file->object_id = $this->owner->id; 78 | $file->ordering = $order++; 79 | $file->save(); 80 | if (!$file->save()) { 81 | throw new ErrorException('Невозможно обновить объект File.'); 82 | } 83 | } 84 | 85 | } 86 | } 87 | } 88 | } 89 | 90 | public function filesDelete() 91 | { 92 | File::deleteAll([ 93 | 'class' => $this->owner->className(), 94 | 'object_id' => $this->owner->id, 95 | ]); 96 | } 97 | 98 | public function validateRequiredFields() 99 | { 100 | foreach ($this->attributes as $attributeName => $params) { 101 | $attributeIds = $this->getRealAttributeName($attributeName); 102 | 103 | if ( 104 | isset($params['required']) && 105 | $params['required'] && 106 | in_array($this->owner->scenario, $params['requiredOn']) && 107 | !in_array($this->owner->scenario, $params['requiredExcept']) && 108 | !isset($this->_values[$attributeIds][1]) 109 | ) 110 | $this->owner->addError($attributeName, $params['requiredMessage']); 111 | } 112 | } 113 | 114 | /** 115 | * Устанавливаем валидаторы. 116 | * @param ActiveRecord $owner 117 | */ 118 | public 119 | function attach($owner) 120 | { 121 | parent::attach($owner); 122 | 123 | // Получаем валидаторы AR 124 | $validators = $owner->validators; 125 | 126 | // Пробегаемся по валидаторам и вычисляем, какие из них касаются наших файл-полей 127 | if ($validators) 128 | foreach ($validators as $key => $validator) { 129 | 130 | // Сначала пробегаемся по файловым валидаторам 131 | if ($validator::className() == 'yii\validators\FileValidator' || $validator::className() == 'floor12\files\components\ReformatValidator') { 132 | foreach ($this->attributes as $field => $params) { 133 | 134 | if (is_string($params)) { 135 | $field = $params; 136 | $params = []; 137 | } 138 | 139 | $index = array_search($field, $validator->attributes); 140 | if ($index !== false) { 141 | $this->attributes[$field]['validator'][$validator::className()] = $validator; 142 | unset($validator->attributes[$index]); 143 | } 144 | } 145 | } 146 | 147 | 148 | if ($validator::className() == 'yii\validators\RequiredValidator') { 149 | foreach ($this->attributes as $field => $params) { 150 | 151 | if (is_string($params)) { 152 | $field = $params; 153 | $params = []; 154 | } 155 | 156 | $index = array_search($field, $validator->attributes); 157 | if ($index !== false) { 158 | unset($validator->attributes[$index]); 159 | $this->attributes[$field]['required'] = true; 160 | $this->attributes[$field]['requiredExcept'] = $validator->except; 161 | $this->attributes[$field]['requiredOn'] = sizeof($validator->on) ? $validator->on : [ActiveRecord::SCENARIO_DEFAULT]; 162 | $this->attributes[$field]['requiredMessage'] = str_replace("{attribute}", $this->owner->getAttributeLabel($field), $validator->message); 163 | } 164 | } 165 | } 166 | 167 | 168 | } 169 | 170 | // Добавляем дефолтный валидатор для прилетающих айдишников 171 | if ($this->attributes) foreach ($this->attributes as $fieldName => $fieldParams) { 172 | $validator = Validator::createValidator('safe', $owner, ["{$fieldName}_ids"]); 173 | $validators->append($validator); 174 | } 175 | } 176 | 177 | 178 | /** 179 | * @inheritdoc 180 | */ 181 | public function canGetProperty($name, $checkVars = true) 182 | { 183 | return array_key_exists($name, $this->attributes) ? 184 | true : parent::canGetProperty($name, $checkVars); 185 | } 186 | 187 | 188 | /** 189 | * @inheritdoc 190 | */ 191 | public function canSetProperty($name, $checkVars = true) 192 | { 193 | if (array_key_exists($this->getRealAttributeName($name), $this->attributes)) 194 | return true; 195 | 196 | return parent::canSetProperty($name, $checkVars = true); 197 | } 198 | 199 | 200 | /** 201 | * @inheritdoc 202 | */ 203 | public function __get($att_name) 204 | { 205 | if (isset($this->_values[$att_name])) { 206 | unset($this->_values[$att_name][0]); 207 | if (sizeof($this->_values[$att_name])) 208 | return array_map(function ($fileId) { 209 | return File::findOne($fileId); 210 | }, $this->_values[$att_name]); 211 | } else { 212 | if (!isset($this->cachedFiles[$att_name])) { 213 | if ( 214 | isset($this->attributes[$att_name]['validator']) && 215 | isset($this->attributes[$att_name]['validator']['yii\validators\FileValidator']) && 216 | $this->attributes[$att_name]['validator']['yii\validators\FileValidator']->maxFiles > 1 217 | ) 218 | $this->cachedFiles[$att_name] = File::find() 219 | ->where( 220 | [ 221 | 'object_id' => $this->owner->id, 222 | 'field' => $att_name, 223 | 'class' => $this->owner->className() 224 | ]) 225 | ->orderBy('ordering ASC') 226 | ->all(); 227 | else { 228 | $this->cachedFiles[$att_name] = File::find() 229 | ->where( 230 | [ 231 | 'object_id' => $this->owner->id, 232 | 'field' => $att_name, 233 | 'class' => $this->owner->className() 234 | ]) 235 | ->orderBy('ordering ASC') 236 | ->one(); 237 | } 238 | } 239 | return $this->cachedFiles[$att_name]; 240 | } 241 | } 242 | 243 | 244 | /** 245 | * @inheritdoc 246 | */ 247 | public 248 | function __set($name, $value) 249 | { 250 | $attribute = $this->getRealAttributeName($name); 251 | 252 | if (array_key_exists($attribute, $this->attributes)) 253 | $this->_values[$attribute] = $value; 254 | } 255 | 256 | 257 | /** Отбрасываем постфикс _ids 258 | * @param $attribute string 259 | * @return string 260 | */ 261 | private 262 | function getRealAttributeName($attribute) 263 | { 264 | return str_replace("_ids", "", $attribute); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/components/FileInputWidget.php: -------------------------------------------------------------------------------- 1 | registerTranslations(); 38 | $this->block_id = rand(9999999, 999999999); 39 | 40 | if (!$this->uploadButtonText) 41 | $this->uploadButtonText = Yii::t('files', 'Upload'); 42 | 43 | $this->ratio = $this->model->getBehavior('files')->attributes[$this->attribute]['ratio'] ?? null; 44 | 45 | if ( 46 | isset($this->model->behaviors['files']->attributes[$this->attribute]['validator']) && 47 | isset($this->model->behaviors['files']->attributes[$this->attribute]['validator']['yii\validators\FileValidator']) && 48 | $this->model->behaviors['files']->attributes[$this->attribute]['validator']['yii\validators\FileValidator']->maxFiles > 1 49 | ) { 50 | $mode = self::MODE_MULTI; 51 | $this->layout = self::VIEW_MULTI; 52 | } 53 | 54 | parent::init(); 55 | } 56 | 57 | public function registerTranslations() 58 | { 59 | $i18n = Yii::$app->i18n; 60 | $i18n->translations['files'] = [ 61 | 'class' => 'yii\i18n\PhpMessageSource', 62 | 'sourceLanguage' => 'en-US', 63 | 'basePath' => '@vendor/floor12/yii2-module-files/src/messages', 64 | ]; 65 | } 66 | 67 | public function run() 68 | { 69 | 70 | $uploadRoute = Url::toRoute(['/files/default/upload']); 71 | $deleteRoute = Url::toRoute(['/files/default/delete']); 72 | $cropperRoute = Url::toRoute(['/files/default/cropper']); 73 | $cropRoute = Url::toRoute(['/files/default/crop']); 74 | $renameRoute = Url::toRoute(['/files/default/rename']); 75 | $altRoute = Url::toRoute(['/files/default/alt']); 76 | 77 | $className = new ClassnameEncoder($this->model->classname()); 78 | 79 | $this->getView()->registerJs("Yii2FilesUploaderSet('files-widget-block_{$this->block_id}','{$className}','{$this->attribute}','{$this->model->scenario}','{$this->name}')", View::POS_READY, $this->block_id); 80 | $this->getView()->registerJs("yii2UploadRoute = '{$uploadRoute}'", View::POS_BEGIN, 'yii2UploadRoute'); 81 | $this->getView()->registerJs("yii2CsrfParam = '" . Yii::$app->request->csrfParam . "'", View::POS_BEGIN, 'yii2CsrfFieldName'); 82 | $this->getView()->registerJs("yii2DeleteRoute = '{$deleteRoute}'", View::POS_BEGIN, 'yii2DeleteRoute'); 83 | $this->getView()->registerJs("yii2CropperRoute = '{$cropperRoute}'", View::POS_BEGIN, 'yii2DeleteRoute'); 84 | $this->getView()->registerJs("yii2CropRoute = '{$cropRoute}'", View::POS_BEGIN, 'yii2CropRoute'); 85 | $this->getView()->registerJs("yii2RenameRoute = '{$renameRoute}'", View::POS_BEGIN, 'yii2RenameRoute'); 86 | $this->getView()->registerJs("yii2AltRoute = '{$altRoute}'", View::POS_BEGIN, 'yii2AltRoute'); 87 | $this->getView()->registerJs("yii2FileFormToken = '" . self::generateToken() . "'", View::POS_BEGIN, 'yii2FileFormToken'); 88 | $this->getView()->registerJs("FileUploadedText = '" . Yii::t('files', 'The file is uploaded') . "'", View::POS_BEGIN, 'FileUploadedText'); 89 | $this->getView()->registerJs("FileSavedText = '" . Yii::t('files', 'The file is saved') . "'", View::POS_BEGIN, 'FileSavedText'); 90 | $this->getView()->registerJs("FileRemovedText = '" . Yii::t('files', 'The file is removed') . "'", View::POS_BEGIN, 'FileRemovedText'); 91 | $this->getView()->registerJs("FilesRemovedText = '" . Yii::t('files', 'The files are removed') . "'", View::POS_BEGIN, 'FilesRemovedText'); 92 | $this->getView()->registerJs("FileRenamedText = '" . Yii::t('files', 'The file is renamed') . "'", View::POS_BEGIN, 'FileRenamedText'); 93 | $this->getView()->registerJs("cropperHideCancel = '{$this->cropperHideCancel}'", View::POS_BEGIN, 'cropperHideCancel'); 94 | 95 | FileInputWidgetAsset::register($this->getView()); 96 | 97 | return $this->render($this->layout, [ 98 | 'className' => $this->model->classname(), 99 | 'uploadButtonText' => $this->uploadButtonText, 100 | 'uploadButtonClass' => $this->uploadButtonClass, 101 | 'block_id' => $this->block_id, 102 | 'scenario' => $this->model->scenario, 103 | 'attribute' => $this->attribute, 104 | 'model' => $this->model, 105 | 'ratio' => $this->ratio, 106 | 'name' => $this->name, 107 | 'value' => $this->value 108 | ]); 109 | } 110 | 111 | /** Генератор токена защиты форм 112 | * @return string 113 | */ 114 | public static function generateToken() 115 | { 116 | return md5(Yii::$app->getModule('files')->token_salt . Yii::$app->request->userAgent . Yii::$app->name); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/components/FileListWidget.php: -------------------------------------------------------------------------------- 1 | files)) 34 | return null; 35 | Yii::$app->getModule('files')->registerTranslations(); 36 | if (empty($this->lightboxKey)) { 37 | $this->lightboxKey = $this->files[0]->field . '-' . $this->files[0]->object_id; 38 | } 39 | parent::init(); 40 | } 41 | 42 | 43 | /** 44 | * @return string|null 45 | */ 46 | public function run() 47 | { 48 | FileListAsset::register($this->getView()); 49 | 50 | $this->getView()->registerJs("yiiDownloadAllLink = '" . Url::toRoute('files/default/zip') . "'", View::POS_BEGIN, 'yiiDownloadAllLink'); 51 | 52 | if ($this->passFirst && sizeof($this->files) > 0) 53 | $this->files = array_slice($this->files, 1); 54 | 55 | 56 | return $this->render('fileListWidget', [ 57 | 'files' => $this->files, 58 | 'zipTitle' => $this->zipTitle, 59 | 'title' => $this->title, 60 | 'downloadAll' => $this->downloadAll, 61 | 'lightboxKey' => $this->lightboxKey, 62 | 'allowImageSrcDownload' => $this->allowImageSrcDownload, 63 | ]); 64 | } 65 | } -------------------------------------------------------------------------------- /src/components/PictureListWidget.php: -------------------------------------------------------------------------------- 1 | models)) { 35 | return ''; 36 | } 37 | 38 | $renderedPictures = []; 39 | 40 | if ($this->passFirst && sizeof($this->models) > 0) 41 | $this->models = array_slice($this->models, 1); 42 | 43 | $lightboxKey = $this->models[0]->field . '-' . $this->models[0]->object_id; 44 | 45 | if ($this->lightbox) { 46 | LightboxAsset::register($this->getView()); 47 | } 48 | 49 | foreach ($this->models as $model) { 50 | 51 | $widget = PictureWidget::widget([ 52 | 'model' => $model, 53 | 'width' => $this->width, 54 | 'classImg' => $this->classImg, 55 | 'classPicture' => $this->classPicture, 56 | 'alt' => $this->alt, 57 | ]); 58 | 59 | if ($this->lightbox) { 60 | $widget = Html::a($widget, $model->getPreviewWebPath($this->lightboxImageWidth), [ 61 | 'data-lightbox' => $lightboxKey 62 | ]); 63 | } 64 | 65 | $renderedPictures[] = Html::tag( 66 | 'li', 67 | $widget, [ 68 | 'class' => $this->classLi 69 | ]); 70 | } 71 | return Html::tag('ul', implode($renderedPictures), [ 72 | 'class' => $this->classUl 73 | ]); 74 | } 75 | } -------------------------------------------------------------------------------- /src/components/PictureWidget.php: -------------------------------------------------------------------------------- 1 | model) || !in_array($this->model->type, [FileType::IMAGE, FileType::VIDEO])) 32 | return null; 33 | 34 | if (is_array($this->width)) 35 | $this->view = 'mediaPictureWidget'; 36 | 37 | return $this->render($this->view, [ 38 | 'model' => $this->model, 39 | 'width' => $this->width, 40 | 'alt' => $this->alt, 41 | 'classPicture' => $this->classPicture, 42 | 'classImg' => $this->classImg, 43 | ]); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/SimpleImage.php: -------------------------------------------------------------------------------- 1 | image_type = $image_info[2]; 25 | if ($this->image_type == IMAGETYPE_JPEG) { 26 | $this->image = imagecreatefromjpeg($filename); 27 | } elseif ($this->image_type == IMAGETYPE_GIF) { 28 | $this->image = imagecreatefromgif($filename); 29 | imageSaveAlpha($this->image, true); 30 | } elseif ($this->image_type == IMAGETYPE_PNG) { 31 | $this->image = @imagecreatefrompng($filename); // https://stackoverflow.com/questions/22745076/libpng-warning-iccp-known-incorrect-srgb-profile 32 | imageSaveAlpha($this->image, true); 33 | } elseif ($this->image_type == IMAGETYPE_WEBP) { 34 | $this->image = imagecreatefromwebp($filename); 35 | } 36 | } 37 | 38 | function save($filename, $image_type = IMAGETYPE_JPEG, $compression = 75, $permissions = null) 39 | { 40 | if ($image_type == IMAGETYPE_JPEG) { 41 | imagejpeg($this->image, $filename, $compression); 42 | } elseif ($image_type == IMAGETYPE_GIF) { 43 | imagegif($this->image, $filename); 44 | } elseif ($image_type == IMAGETYPE_PNG) { 45 | imagepng($this->image, $filename); 46 | } elseif ($image_type == IMAGETYPE_WEBP) { 47 | $dst = imagecreatetruecolor(imagesx($this->image), imagesy($this->image)); 48 | imagealphablending($dst, false); 49 | imagesavealpha($dst, true); 50 | $transparent = imagecolorallocatealpha($dst, 255, 255, 255, 127); 51 | imagefilledrectangle($dst, 0, 0, imagesx($this->image), imagesy($this->image), $transparent); 52 | imagecopy($dst, $this->image, 0, 0, 0, 0, imagesx($this->image), imagesy($this->image)); 53 | imagewebp($dst, $filename); 54 | } 55 | 56 | if ($permissions != null) { 57 | chmod($filename, $permissions); 58 | } 59 | } 60 | 61 | function output($image_type = IMAGETYPE_JPEG) 62 | { 63 | ob_start(); 64 | if ($image_type == IMAGETYPE_JPEG) { 65 | imagejpeg($this->image); 66 | } elseif ($image_type == IMAGETYPE_GIF) { 67 | imagegif($this->image); 68 | } elseif ($image_type == IMAGETYPE_PNG) { 69 | imagepng($this->image); 70 | } elseif ($image_type == IMAGETYPE_WEBP) { 71 | $dst = imagecreatetruecolor(imagesx($this->image), imagesy($this->image)); 72 | imagealphablending($dst, false); 73 | imagesavealpha($dst, true); 74 | $transparent = imagecolorallocatealpha($dst, 255, 255, 255, 127); 75 | imagefilledrectangle($dst, 0, 0, imagesx($this->image), imagesy($this->image), $transparent); 76 | imagecopy($dst, $this->image, 0, 0, 0, 0, imagesx($this->image), imagesy($this->image)); 77 | imagewebp($dst); 78 | } 79 | return ob_get_clean(); 80 | } 81 | 82 | function resizeToHeight($height) 83 | { 84 | $ratio = $height / $this->getHeight(); 85 | $width = $this->getWidth() * $ratio; 86 | $this->resize($width, $height); 87 | } 88 | 89 | function getHeight() 90 | { 91 | try { 92 | return imagesy($this->image); 93 | } catch (\Throwable $exception) { 94 | throw new ErrorException('Unable to get height of image. Probably the image is corrupted.'); 95 | } 96 | 97 | } 98 | 99 | function getWidth() 100 | { 101 | try { 102 | return imagesx($this->image); 103 | } catch (\Throwable $exception) { 104 | throw new ErrorException('Unable to get width of image. Probably the image is corrupted.'); 105 | } 106 | } 107 | 108 | function resize($width, $height) 109 | { 110 | $new_image = imagecreatetruecolor($width, $height); 111 | imagealphablending($new_image, false); 112 | imagesavealpha($new_image, true); 113 | $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127); 114 | imagefilledrectangle($new_image, 0, 0, $width, $height, $transparent); 115 | imagecopyresampled($new_image, $this->image, 0, 0, 0, 0, $width, $height, $this->getWidth(), $this->getHeight()); 116 | $this->image = $new_image; 117 | } 118 | 119 | function resizeToWidth($width) 120 | { 121 | $ratio = $width / $this->getWidth(); 122 | $height = $this->getheight() * $ratio; 123 | $this->resize((int)$width, (int)$height); 124 | } 125 | 126 | function scale($scale) 127 | { 128 | $width = $this->getWidth() * $scale / 100; 129 | $height = $this->getheight() * $scale / 100; 130 | $this->resize((int)$width, (int)$height); 131 | } 132 | 133 | function rotate($direction) 134 | { 135 | $degrees = 90; 136 | if ($direction == 2) 137 | $degrees = 270; 138 | $this->image = imagerotate($this->image, $degrees, 0); 139 | } 140 | 141 | function rotateDegrees($degrees) 142 | { 143 | $this->image = imagerotate($this->image, $degrees, 0); 144 | } 145 | 146 | 147 | public function watermark($path) 148 | { 149 | $stamp = imagecreatefrompng($path); 150 | 151 | $transparentStamp = imagecreatetruecolor($this->getWidth(), $this->getHeight()); 152 | imagealphablending($transparentStamp, false); 153 | imagesavealpha($transparentStamp, true); 154 | $transparent = imagecolorallocatealpha($transparentStamp, 255, 255, 255, 127); 155 | imagecolortransparent($transparentStamp, $transparent); 156 | imagefilledrectangle($transparentStamp, 0, 0, $this->getWidth(), $this->getHeight(), $transparent); 157 | imagecopyresampled($transparentStamp, $stamp, 0, 0, 0, 0, $this->getWidth(), $this->getHeight(), $this->getWidth(), $this->getHeight()); 158 | 159 | $newImage = imagecreatetruecolor($this->getWidth(), $this->getHeight()); 160 | imagecopyresampled($newImage, $this->image, 0, 0, 0, 0, $this->getWidth(), $this->getHeight(), $this->getWidth(), $this->getHeight()); 161 | imagecopyresampled($newImage, $transparentStamp, 0, 0, 0, 0, $this->getWidth(), $this->getHeight(), $this->getWidth(), $this->getHeight()); 162 | $this->image = $newImage; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/components/VideoWidget.php: -------------------------------------------------------------------------------- 1 | model->type !== FileType::VIDEO) 25 | return null; 26 | $source = Html::tag('source', null, ['src' => $this->model->getHref(), 'type' => $this->model->content_type]); 27 | return Html::tag('video', $source, $this->options); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/views/_fileListWidget.php: -------------------------------------------------------------------------------- 1 | 31 |
  • 32 | type == FileType::IMAGE) { ?> 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | 47 | getModule('files')->allowOfficePreview && in_array($model->content_type, $doc_contents)) { ?> 48 | 50 | 51 | 52 | 53 | 54 | 56 | icon ?> 57 | title ?> 58 | 59 | 60 | 61 |
  • -------------------------------------------------------------------------------- /src/components/views/fileListWidget.php: -------------------------------------------------------------------------------- 1 | 25 | 26 |
    27 | 28 | 29 | 30 |
      31 | render('_fileListWidget', [ 33 | 'model' => $file, 34 | 'lightboxKey' => $lightboxKey, 35 | 'allowImageSrcDownload' => $allowImageSrcDownload 36 | ]); 37 | } ?> 38 | 1) { ?> 39 |
    • 40 | 42 | 43 | 44 | 45 |
    • 46 | 47 |
    48 |
    -------------------------------------------------------------------------------- /src/components/views/mediaPictureWidget.php: -------------------------------------------------------------------------------- 1 | 15 | 16 | > 17 | $widthValue) { ?> 18 | 24 | 25 | $widthValue) { ?> 26 | 32 | 33 | <?= $alt ?>> 34 |
    35 | -------------------------------------------------------------------------------- /src/components/views/multiFileInputWidget.php: -------------------------------------------------------------------------------- 1 | "files-upload-field-{$attribute}", 31 | 'class' => 'yii2-files-upload-field', 32 | 'data-modelclass' => $model::className(), 33 | 'data-attribute' => $attribute, 34 | 'data-mode' => 'multi', 35 | 'data-ratio' => $ratio ?? 0, 36 | 'data-block' => $block_id, 37 | 38 | ]) ?> 39 | 40 |
    44 | 48 | getShortName() . "[{$attribute}_ids][]", null) ?> 49 |
    50 | $attribute) foreach ($model->$attribute as $file) echo $this->render('@vendor/floor12/yii2-module-files/src/views/default/_file', ['model' => $file, 'ratio' => $ratio]) ?> 51 |
    52 |
    53 |
    54 | -------------------------------------------------------------------------------- /src/components/views/pictureWidget.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | > 16 | 21 | 26 | <?= $alt ?>> 27 | 28 | -------------------------------------------------------------------------------- /src/components/views/singleFileInputWidget.php: -------------------------------------------------------------------------------- 1 | "files-upload-field-{$attribute}", 32 | 'class' => 'yii2-files-upload-field', 33 | 'data-modelclass' => $model::className(), 34 | 'data-attribute' => $attribute, 35 | 'data-mode' => 'single', 36 | 'data-name' => $name ?: (new ReflectionClass($model))->getShortName() . "[{$attribute}_ids][]", 37 | 'data-ratio' => $ratio ?? 0, 38 | 'data-block' => $block_id, 39 | 40 | ]) ?> 41 | 42 |
    44 | 48 | getShortName() . "[{$attribute}_ids][]", null) ?> 49 |
    50 | $attribute) echo $this->render('@vendor/floor12/yii2-module-files/src/views/default/_single', [ 51 | 'model' => $value ?? $model->$attribute, 52 | 'ratio' => $ratio, 53 | 'name' => $name 54 | ]) ?> 55 |
    56 |
    57 |
    58 | -------------------------------------------------------------------------------- /src/controllers/ConsoleController.php: -------------------------------------------------------------------------------- 1 | where(['object_id' => '0'])->andWhere(['<', 'created', $time])->all(); 35 | if ($files) foreach ($files as $file) { 36 | $file->delete(); 37 | } 38 | } 39 | 40 | function actionClear() 41 | { 42 | $countDeleted = $countOk = 0; 43 | $module = Yii::$app->getModule('files'); 44 | $path1 = $module->storageFullPath; 45 | foreach (scandir($path1) as $folder1) { 46 | $path2 = $path1 . '/' . $folder1; 47 | if ($this->checkFolderItem($folder1)) { 48 | continue; 49 | } 50 | foreach (scandir($path2) as $folder2) { 51 | $path3 = $path2 . '/' . $folder2; 52 | if ($this->checkFolderItem($folder2)) { 53 | continue; 54 | }; 55 | foreach (scandir($path3) as $filename) { 56 | $path4 = $path3 . '/' . $filename; 57 | if ($this->checkFolderItem($filename)) { 58 | continue; 59 | }; 60 | $dbFileName = "/{$folder1}/{$folder2}/{$filename}"; 61 | if (is_file($path4)) { 62 | $this->stdout($path4 . "..."); 63 | if (File::find()->where(['filename' => $dbFileName])->count() === 0) { 64 | $this->stdout('no' . PHP_EOL, Console::FG_RED); 65 | unlink($path4); 66 | $countDeleted++; 67 | } else { 68 | $this->stdout('ok' . PHP_EOL, Console::FG_GREEN); 69 | $countOk++; 70 | } 71 | } 72 | } 73 | } 74 | } 75 | $this->stdout('Deleted: ' . $countDeleted . PHP_EOL, Console::FG_YELLOW); 76 | $this->stdout('Ok: ' . $countOk . PHP_EOL, Console::FG_GREEN); 77 | } 78 | 79 | private function checkFolderItem($string) 80 | { 81 | $ignoreItems = ['.', '..', '.gitignore', 'summerfiles']; 82 | if (in_array($string, $ignoreItems)) { 83 | return true; 84 | } 85 | return false; 86 | } 87 | 88 | 89 | /** 90 | * Run `./yii files/console/clean-cache` to remove all generated images and previews 91 | */ 92 | function actionCleanCache() 93 | { 94 | $module = Yii::$app->getModule('files'); 95 | $commands = []; 96 | $commands[] = "find {$module->storageFullPath} -regextype egrep -regex \".+/.{32}_.*\" -exec rm -rf {} \;"; 97 | $commands[] = "find {$module->cacheFullPath} -regextype egrep -regex \".+/.{32}_.*\" -exec rm -rf {} \;"; 98 | $commands[] = "find {$module->storageFullPath} -regextype egrep -regex \".+/.{32}\..{3,4}\.jpg\" -exec rm -rf {} \;"; 99 | $commands[] = "find {$module->cacheFullPath} -regextype egrep -regex \".+/.{32}\..{3,4}\.jpg\" -exec rm -rf {} \;"; 100 | 101 | array_map(function ($command) { 102 | exec($command); 103 | }, $commands); 104 | 105 | } 106 | 107 | /** 108 | * Run `./yii files/console/convert` to proccess one video file from queue with ffmpeg 109 | * @return bool|int 110 | */ 111 | function actionConvert() 112 | { 113 | $ffmpeg = Yii::$app->getModule('files')->ffmpeg; 114 | 115 | if (!file_exists($ffmpeg)) 116 | return $this->stdout("ffmpeg is not found: {$ffmpeg}" . PHP_EOL, Console::FG_RED); 117 | 118 | if (!is_executable($ffmpeg)) 119 | return $this->stdout("ffmpeg is not executable: {$ffmpeg}" . PHP_EOL, Console::FG_RED); 120 | 121 | $file = File::find() 122 | ->where(['type' => FileType::VIDEO, 'video_status' => VideoStatus::QUEUE]) 123 | ->andWhere(['!=', 'object_id', 0]) 124 | ->one(); 125 | 126 | if (!$file) 127 | return $this->stdout("Convert queue is empty" . PHP_EOL, Console::FG_GREEN); 128 | 129 | if (!file_exists($file->rootPath)) 130 | return $this->stdout("Source file is not found: {$file->rootPath}" . PHP_EOL, Console::FG_RED); 131 | 132 | 133 | $file->video_status = VideoStatus::CONVERTING; 134 | $file->save(); 135 | $width = $this->getVideoWidth($file->class, $file->field); 136 | $height = $this->getVideoHeight($file->class, $file->field); 137 | $newFileName = $file->filename . ".mp4"; 138 | $newFilePath = $file->rootPath . ".mp4"; 139 | $command = Yii::$app->getModule('files')->ffmpeg . " -i {$file->rootPath} -vf scale={$width}:{$height} -threads 4 {$newFilePath}"; 140 | echo $command . PHP_EOL; 141 | exec($command, 142 | $outout, $result); 143 | if ($result == 0) { 144 | @unlink($file->rootPath); 145 | $file->filename = $newFileName; 146 | $file->content_type = 'video/mp4'; 147 | $file->video_status = VideoStatus::READY; 148 | } else { 149 | $file->video_status = VideoStatus::QUEUE; 150 | } 151 | $file->save(); 152 | 153 | return $this->stdout("File converted: {$file->rootPath}" . PHP_EOL, Console::FG_GREEN); 154 | } 155 | 156 | protected 157 | function getVideoWidth($classname, $field) 158 | { 159 | /** @var ActiveRecord $ownerClassObject */ 160 | $ownerClassObject = new $classname; 161 | return $ownerClassObject->getBehavior('files')->attributes[$field]['videoWidth'] ?? 1280; 162 | } 163 | 164 | protected 165 | function getVideoHeight($classname, $field) 166 | { 167 | /** @var ActiveRecord $ownerClassObject */ 168 | $ownerClassObject = new $classname; 169 | return $ownerClassObject->getBehavior('files')->attributes[$field]['videoHeight'] ?? -1; 170 | } 171 | 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/controllers/DefaultController.php: -------------------------------------------------------------------------------- 1 | [ 48 | 'class' => VerbFilter::class, 49 | 'actions' => [ 50 | 'zip' => ['GET', 'HEAD'], 51 | 'cropper' => ['GET', 'HEAD'], 52 | 'crop' => ['POST', 'HEAD'], 53 | 'rename' => ['POST', 'HEAD'], 54 | 'upload' => ['POST', 'HEAD'], 55 | 'get' => ['GET', 'HEAD'], 56 | 'preview' => ['GET', 'HEAD'], 57 | ], 58 | ], 59 | ]; 60 | } 61 | 62 | 63 | /** 64 | * @inheritdoc 65 | */ 66 | public function beforeAction($action) 67 | { 68 | $this->checkFormToken(); 69 | return parent::beforeAction($action); 70 | } 71 | 72 | /** Првоеряем токен 73 | * @throws BadRequestHttpException 74 | */ 75 | private function checkFormToken() 76 | { 77 | if (in_array($this->action->id, $this->actionsToCheck) && FileInputWidget::generateToken() != Yii::$app->request->post('_fileFormToken')) 78 | throw new BadRequestHttpException('File-form token is wrong or missing.'); 79 | } 80 | 81 | /** 82 | * @param array $hash 83 | * @param string $title 84 | */ 85 | public function actionZip(array $hash, $title = 'files') 86 | { 87 | $md5 = md5(serialize($hash)); 88 | $files = File::find()->where(["IN", "hash", $hash])->all(); 89 | 90 | $zip = new ZipArchive; 91 | $path = Yii::getAlias("@runtime/zip"); 92 | if (!file_exists($path)) { 93 | mkdir($path, 0777, true); 94 | } 95 | $filename = "{$path}/{$md5}.zip"; 96 | if (file_exists($filename)) 97 | @unlink($filename); 98 | if (sizeof($files) && $zip->open($filename, ZipArchive::CREATE)) { 99 | foreach ($files as $file) { 100 | $zip->addFile($file->rootPath, $file->title); 101 | } 102 | $zip->close(); 103 | return Yii::$app->response->sendFile($filename, "{$title}.zip"); 104 | } else { 105 | throw new NotFoundHttpException("Error while zipping files."); 106 | } 107 | } 108 | 109 | /** Возвращаем HTML шаблон для внедрения в основной макет 110 | * @return string 111 | */ 112 | public function actionCropper() 113 | { 114 | return $this->renderPartial('_cropper'); 115 | } 116 | 117 | /** Кропаем и поворачиваем картинку, возращая ее новый адрес. 118 | * @return string 119 | * @throws InvalidConfigException 120 | * @throws \yii\base\ErrorException 121 | */ 122 | public function actionCrop() 123 | { 124 | return Yii::createObject(FileCropRotate::class, [Yii::$app->request->post()])->execute(); 125 | } 126 | 127 | /** Переименовываем файл 128 | * @return string 129 | * @throws BadRequestHttpException 130 | * @throws InvalidConfigException 131 | */ 132 | public function actionRename() 133 | { 134 | return Yii::createObject(FileRename::class, [Yii::$app->request->post()])->execute(); 135 | } 136 | 137 | /** Alt 138 | * @return string 139 | * @throws BadRequestHttpException 140 | * @throws InvalidConfigException 141 | */ 142 | public function actionAlt() 143 | { 144 | return Yii::createObject(FileAlt::class, [Yii::$app->request->post()])->execute(); 145 | } 146 | 147 | 148 | /** Создаем новый файл 149 | * @return string 150 | * @throws BadRequestHttpException 151 | * @throws InvalidConfigException 152 | */ 153 | public function actionUpload() 154 | { 155 | $model = Yii::createObject(FileCreateFromInstance::class, [ 156 | UploadedFile::getInstanceByName('file'), 157 | Yii::$app->request->post(), 158 | Yii::$app->user->identity, 159 | ])->execute(); 160 | 161 | 162 | if ($model->errors) { 163 | throw new BadRequestHttpException('Ошибки валидации модели'); 164 | } 165 | 166 | $ratio = Yii::$app->request->post('ratio') ?? null; 167 | 168 | $name = Yii::$app->request->post('name') ?? null; 169 | 170 | $view = Yii::$app->request->post('mode') == 'single' ? "_single" : "_file"; 171 | 172 | if ($ratio) 173 | $this->getView()->registerJs("initCropper({$model->id}, '{$model->href}', {$ratio}, true);"); 174 | 175 | return $this->renderAjax($view, [ 176 | 'model' => $model, 177 | 'ratio' => $ratio, 178 | 'name' => $name 179 | ]); 180 | } 181 | 182 | /** 183 | * @return array|string[] 184 | */ 185 | public function actions() 186 | { 187 | return [ 188 | 'get' => GetFileAction::class, 189 | 'image' => GetPreviewAction::class 190 | ]; 191 | } 192 | 193 | /* 194 | * Выдача файлов через контроллер. 195 | */ 196 | public function actionPreview($hash) 197 | { 198 | $model = File::findOne(['hash' => $hash]); 199 | 200 | if (!$model) 201 | throw new NotFoundHttpException("Запрашиваемый файл не найден в базе."); 202 | 203 | $response = Yii::$app->response; 204 | $response->format = Response::FORMAT_RAW; 205 | $response->getHeaders()->set('Content-Type', 'image/jpeg; charset=utf-8'); 206 | 207 | Yii::$app->response->headers->set('Last-Modified', date("c", $model->created)); 208 | Yii::$app->response->headers->set('Cache-Control', 'public, max-age=' . (60 * 60 * 24 * 15)); 209 | 210 | if (!file_exists($model->getRootPreviewPath())) 211 | throw new NotFoundHttpException('Preview not found.'); 212 | 213 | $response->sendFile($model->getRootPreviewPath(), 'preview.jpg'); 214 | 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/logic/ClassnameEncoder.php: -------------------------------------------------------------------------------- 1 | encoded = str_replace("\\", "\\\\", $className); 29 | } 30 | 31 | /** 32 | * @return mixed|string 33 | */ 34 | public function __toString() 35 | { 36 | return $this->encoded; 37 | } 38 | } -------------------------------------------------------------------------------- /src/logic/FileAlt.php: -------------------------------------------------------------------------------- 1 | alt = $data['alt']; 37 | 38 | $this->_file = File::findOne($data['id']); 39 | 40 | if (!$this->_file) 41 | throw new NotFoundHttpException('File not found.'); 42 | 43 | } 44 | 45 | /** 46 | * @return string 47 | * @throws BadRequestHttpException 48 | */ 49 | public function execute() 50 | { 51 | $this->_file->alt = $this->alt; 52 | 53 | if (!$this->_file->save()) 54 | throw new BadRequestHttpException('Unable to save file.'); 55 | 56 | return $this->alt; 57 | } 58 | } -------------------------------------------------------------------------------- /src/logic/FileCreateFromInstance.php: -------------------------------------------------------------------------------- 1 | _onlyUploaded = $onlyUploaded; 37 | 38 | if (!isset($data['attribute']) || !$data['attribute'] || !isset($data['modelClass']) || !$data['modelClass']) 39 | throw new BadRequestHttpException("Attribute or class name not set."); 40 | 41 | // Загружаем полученные данные 42 | $this->_instance = $file; 43 | $this->_attribute = $data['attribute']; 44 | 45 | if (!file_exists($this->_instance->tempName)) 46 | throw new ErrorException("Tmp file not found on disk."); 47 | 48 | // Инициализируем класс владельца файла для валидаций и ставим сценарий 49 | $this->_owner = new $data['modelClass']; 50 | 51 | if (isset($data['scenario'])) 52 | $this->_owner->setScenario($data['scenario']); 53 | 54 | 55 | if (isset($this->_owner->behaviors['files']->attributes[$this->_attribute]['validator'])) { 56 | foreach ($this->_owner->behaviors['files']->attributes[$this->_attribute]['validator'] as $validator) { 57 | if ($validator->maxFiles && (int)$data['count'] > $validator->maxFiles) { 58 | throw new BadRequestHttpException('The maximum number of files has been exceeded: ' . $validator->maxFiles); 59 | } 60 | 61 | if (!$validator->validate($this->_instance, $error)) 62 | throw new BadRequestHttpException($error); 63 | } 64 | 65 | } 66 | 67 | // Создаем модель нового файла и заполняем первоначальными данными 68 | $this->_model = new File(); 69 | $this->_model->created = time(); 70 | $this->_model->field = $this->_attribute; 71 | $this->_model->class = $data['modelClass']; 72 | 73 | $this->_model->filename = new PathGenerator(Yii::$app->getModule('files')->storageFullPath) . '.' . $this->_instance->extension; 74 | $this->_model->title = $this->_instance->name; 75 | $this->_model->content_type = \yii\helpers\FileHelper::getMimeType($this->_instance->tempName); 76 | $this->_model->size = $this->_instance->size; 77 | $this->_model->type = $this->detectType(); 78 | if ($identity) 79 | $this->_model->user_id = $identity->getId(); 80 | if ($this->_model->type == FileType::VIDEO) 81 | $this->_model->video_status = 0; 82 | 83 | //Генерируем полный новый адрес сохранения файла 84 | $this->_fullPath = Yii::$app->getModule('files')->storageFullPath . DIRECTORY_SEPARATOR . $this->_model->filename; 85 | } 86 | 87 | /** 88 | * @return string 89 | */ 90 | public function detectType() 91 | { 92 | $contentTypeArray = explode('/', $this->_model->content_type); 93 | if ($contentTypeArray[0] == 'image') 94 | return FileType::IMAGE; 95 | if ($contentTypeArray[0] == 'video') 96 | return FileType::VIDEO; 97 | return FileType::FILE; 98 | } 99 | 100 | /** 101 | * @return File 102 | */ 103 | 104 | public function execute() 105 | { 106 | $path = Yii::$app->getModule('files')->storageFullPath . $this->_model->filename; 107 | 108 | if ($this->_model->save()) { 109 | if (!$this->_onlyUploaded) 110 | copy($this->_instance->tempName, $this->_fullPath); 111 | else 112 | $this->_instance->saveAs($this->_fullPath, false); 113 | } 114 | 115 | if ($this->_model->type == FileType::IMAGE) { 116 | $this->rotateAfterUpload(); 117 | $this->resizeAfterUpload(); 118 | } 119 | 120 | return $this->_model; 121 | } 122 | 123 | 124 | protected function rotateAfterUpload() 125 | { 126 | $exif = ''; 127 | @$exif = exif_read_data($this->_fullPath); 128 | if (isset($exif['Orientation'])) { 129 | $ort = $exif['Orientation']; 130 | $rotatingImage = new SimpleImage(); 131 | $rotatingImage->load($this->_fullPath); 132 | switch ($ort) { 133 | case 3: // 180 rotate left 134 | $rotatingImage->rotateDegrees(180); 135 | $rotatingImage->save($this->_fullPath); 136 | break; 137 | case 6: // 90 rotate right 138 | $rotatingImage->rotateDegrees(270); 139 | $rotatingImage->save($this->_fullPath); 140 | break; 141 | case 8: // 90 rotate left 142 | $rotatingImage->rotateDegrees(90); 143 | $rotatingImage->save($this->_fullPath); 144 | } 145 | 146 | } 147 | } 148 | 149 | protected function resizeAfterUpload() 150 | { 151 | $maxWidth = $this->_owner->behaviors['files']->attributes[$this->_attribute]['maxWidth'] ?? 0; 152 | $maxHeight = $this->_owner->behaviors['files']->attributes[$this->_attribute]['maxHeight'] ?? 0; 153 | 154 | if ($maxWidth && $maxHeight) { 155 | $resizer = new FileResize($this->_model, $maxWidth, $maxHeight); 156 | $resizer->execute(); 157 | } 158 | 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/logic/FileCreateFromPath.php: -------------------------------------------------------------------------------- 1 | model = $model; 31 | 32 | if (!$filePath || !$className || !$fieldName || !$storagePath) 33 | throw new ErrorException("Empty params not allowed."); 34 | 35 | if (!file_exists($storagePath)) 36 | throw new ErrorException("File storage not found on disk."); 37 | 38 | if (!file_exists($filePath)) 39 | throw new ErrorException("File not found on disk."); 40 | 41 | if (!is_writable($storagePath)) 42 | throw new ErrorException("File storage is not writable."); 43 | $this->filePath = $filePath; 44 | $this->fileName = $fileName; 45 | $this->fieldName = $fieldName; 46 | $this->className = $className; 47 | $this->storagePath = $storagePath; 48 | 49 | } 50 | 51 | /** Основная работка 52 | * @return bool 53 | * @throws ErrorException 54 | */ 55 | public function execute() 56 | { 57 | // копируем файл в хранилище 58 | $tmp_extansion = explode('?', pathinfo($this->filePath, PATHINFO_EXTENSION)); 59 | $extansion = $tmp_extansion[0]; 60 | $filename = new PathGenerator($this->storagePath) . "." . $extansion; 61 | $new_path = $this->storagePath . $filename; 62 | copy($this->filePath, $new_path); 63 | 64 | // создаем запись в базе 65 | $this->model->field = $this->fieldName; 66 | $this->model->class = $this->className; 67 | $this->model->filename = $filename; 68 | if ($this->model->filename) 69 | $this->model->title = $this->model->filename; 70 | else 71 | $this->model->title = rand(0, 99999); #такой прикол ) 72 | $this->model->content_type = $this->model->mime_content_type($new_path); 73 | $this->model->type = $this->detectType(); 74 | $this->model->size = filesize($new_path); 75 | $this->model->created = time(); 76 | if ($this->model->type == FileType::VIDEO) 77 | $this->model->video_status = 0; 78 | 79 | if ($this->model->save()) { 80 | 81 | if ($this->model->type == FileType::IMAGE) { 82 | $exif = ''; 83 | @$exif = exif_read_data($new_path); 84 | if (isset($exif['Orientation'])) { 85 | $ort = $exif['Orientation']; 86 | $rotatingImage = new SimpleImage(); 87 | $rotatingImage->load($new_path); 88 | switch ($ort) { 89 | 90 | case 3: // 180 rotate left 91 | $rotatingImage->rotateDegrees(180); 92 | break; 93 | case 6: // 90 rotate right 94 | $rotatingImage->rotateDegrees(270); 95 | break; 96 | case 8: // 90 rotate left 97 | $rotatingImage->rotateDegrees(90); 98 | } 99 | $rotatingImage->save($new_path); 100 | } 101 | } 102 | return true; 103 | } 104 | return false; 105 | } 106 | 107 | /** 108 | * @return integer 109 | */ 110 | private function detectType() 111 | { 112 | $contentTypeArray = explode('/', $this->model->content_type); 113 | if ($contentTypeArray[0] == 'image') 114 | return FileType::IMAGE; 115 | if ($contentTypeArray[0] == 'video') 116 | return FileType::VIDEO; 117 | return FileType::FILE; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/logic/FileCropRotate.php: -------------------------------------------------------------------------------- 1 | _file = File::findOne($data['id']); 32 | 33 | if (!$this->_file) 34 | throw new NotFoundHttpException('File not found'); 35 | 36 | if ($this->_file->type != FileType::IMAGE) 37 | throw new BadRequestHttpException('Requested file is not an image.'); 38 | 39 | if (!file_exists($this->_file->rootPath)) 40 | throw new BadRequestHttpException('File not found in file storage.'); 41 | 42 | $this->_height = (int)$data['height']; 43 | $this->_width = (int)$data['width']; 44 | $this->_top = (int)$data['top']; 45 | $this->_left = (int)$data['left']; 46 | $this->_rotated = (int)($data['rotated'] ?? 0); 47 | 48 | if (!$this->_height && !$this->_width) { 49 | list($this->_width, $this->_height) = getimagesize($this->_file->rootPath); 50 | } 51 | 52 | } 53 | 54 | public function execute() 55 | { 56 | 57 | $src = $this->imageCreateFromAny(); 58 | 59 | $src = imagerotate($src, -$this->_rotated, 0); 60 | 61 | $dest = imagecreatetruecolor($this->_width, $this->_height); 62 | 63 | imagecopy($dest, $src, 0, 0, $this->_left, $this->_top, $this->_width, $this->_height); 64 | 65 | $newName = new PathGenerator(Yii::$app->getModule('files')->storageFullPath) . '.jpeg'; 66 | 67 | $newPath = Yii::$app->getModule('files')->storageFullPath . '/' . $newName; 68 | 69 | $oldPath = $this->_file->rootPath; 70 | 71 | imagejpeg($dest, $newPath, 80); 72 | 73 | imagedestroy($dest); 74 | 75 | imagedestroy($src); 76 | 77 | $this->_file->filename = $newName; 78 | $this->_file->content_type = $this->_file->mime_content_type($newPath); 79 | $this->_file->size = filesize($newPath); 80 | $this->_file->changeHash(); 81 | if ($this->_file->save()) { 82 | @unlink($oldPath); 83 | return $this->_file->href; 84 | } else 85 | throw new ErrorException("Error while saving file model."); 86 | 87 | 88 | } 89 | 90 | 91 | /** 92 | * Method to read files from any mime types 93 | * @return resource 94 | * @throws BadRequestHttpException 95 | */ 96 | 97 | private function imageCreateFromAny() 98 | { 99 | $type = exif_imagetype($this->_file->rootPath); 100 | $allowedTypes = array( 101 | 1, // [] gif 102 | 2, // [] jpg 103 | 3, // [] png 104 | 6 // [] bmp 105 | ); 106 | if (!in_array($type, $allowedTypes)) { 107 | throw new BadRequestHttpException('File must have GIF, JPG, PNG or BMP mime-type.'); 108 | 109 | } 110 | switch ($type) { 111 | case 1 : 112 | $im = imageCreateFromGif($this->_file->rootPath); 113 | break; 114 | case 2 : 115 | $im = imageCreateFromJpeg($this->_file->rootPath); 116 | break; 117 | case 3 : 118 | $im = imageCreateFromPng($this->_file->rootPath); 119 | break; 120 | case 6 : 121 | $im = imageCreateFromBmp($this->_file->rootPath); 122 | break; 123 | } 124 | return $im; 125 | } 126 | } -------------------------------------------------------------------------------- /src/logic/FileRename.php: -------------------------------------------------------------------------------- 1 | _title = $data['title']; 37 | 38 | $this->_file = File::findOne($data['id']); 39 | 40 | if (!$this->_file) 41 | throw new NotFoundHttpException('File not found.'); 42 | 43 | } 44 | 45 | /** 46 | * @return string 47 | * @throws BadRequestHttpException 48 | */ 49 | public function execute() 50 | { 51 | $this->_file->title = $this->_title; 52 | 53 | if (!$this->_file->save()) 54 | throw new BadRequestHttpException('Unable to save file.'); 55 | 56 | return $this->_title; 57 | } 58 | } -------------------------------------------------------------------------------- /src/logic/FileResize.php: -------------------------------------------------------------------------------- 1 | _file = $file; 35 | 36 | if ($this->_file->type != FileType::IMAGE) 37 | throw new ErrorException('This file is not an image.'); 38 | 39 | if (!file_exists($this->_file->rootPath)) 40 | throw new ErrorException('File not found on disk'); 41 | 42 | $this->_maxHeight = $maxHeight; 43 | $this->_maxWidth = $maxWidth; 44 | $this->_compression = $compression; 45 | 46 | } 47 | 48 | 49 | /** Непосредственная обработка 50 | * @return bool 51 | * @throws ErrorException 52 | */ 53 | public function execute(): bool 54 | { 55 | if ($this->_file->content_type == 'image/svg+xml') 56 | return true; 57 | 58 | $image = new SimpleImage(); 59 | $image->load($this->_file->rootPath); 60 | 61 | if ($image->getWidth() > $this->_maxWidth || $image->getHeight() > $this->_maxHeight) { 62 | $image->resizeToWidth($this->_maxWidth); 63 | if ($this->_file->content_type == 'image/png') 64 | $this->_imageType = IMAGETYPE_PNG; 65 | $image->save($this->_file->rootPath, $this->_imageType, $this->_compression); 66 | $this->_file->size = filesize($this->_file->rootPath); 67 | return $this->_file->save(false, ['size']); 68 | } 69 | return true; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/logic/ImagePreviewer.php: -------------------------------------------------------------------------------- 1 | model = $model; 30 | $this->width = $width; 31 | $this->webp = $webp; 32 | 33 | if (!$this->model->isImage() && !$this->model->isVideo()) 34 | throw new ErrorException('File is not an image or video.'); 35 | } 36 | 37 | /** 38 | * @return string 39 | * @throws \ErrorException 40 | * @throws \yii\base\InvalidConfigException 41 | */ 42 | public function getUrl() 43 | { 44 | if ($this->model->isSvg()) 45 | return $this->model->getRootPath(); 46 | 47 | $cachePath = Yii::$app->getModule('files')->cacheFullPath; 48 | $jpegName = $this->model->makeNameWithSize($this->model->filename, $this->width); 49 | $webpName = $this->model->makeNameWithSize($this->model->filename, $this->width, true); 50 | 51 | $this->fileName = $cachePath . DIRECTORY_SEPARATOR . $jpegName; 52 | $this->fileNameWebp = $cachePath . DIRECTORY_SEPARATOR . $webpName; 53 | 54 | $this->prepareFolder(); 55 | 56 | $sourceImagePath = $this->model->rootPath; 57 | if ($this->model->isVideo()) { 58 | $sourceImagePath = $this->fileName . '.jpeg'; 59 | if (!is_file($sourceImagePath)) 60 | Yii::createObject(VideoFrameExtractor::class, [ 61 | $this->model->rootPath, 62 | $sourceImagePath 63 | ])->extract(); 64 | } 65 | 66 | if (!is_file($this->fileName) || filesize($this->fileName) == 0) 67 | $this->createPreview($sourceImagePath, $this->model->getWatermark()); 68 | 69 | if ($this->webp && !file_exists($this->fileNameWebp)) 70 | $this->createPreviewWebp(); 71 | 72 | if ($this->webp) 73 | return $this->fileNameWebp; 74 | 75 | return $this->fileName; 76 | } 77 | 78 | /** 79 | * Generate all folders for storing image thumbnails cache. 80 | */ 81 | protected function prepareFolder() 82 | { 83 | if (!file_exists(Yii::$app->getModule('files')->cacheFullPath)) 84 | mkdir(Yii::$app->getModule('files')->cacheFullPath); 85 | $lastFolder = '/'; 86 | $explodes = explode('/', $this->fileName); 87 | array_pop($explodes); 88 | if (empty($explodes)) 89 | return; 90 | foreach ($explodes as $folder) { 91 | if (empty($folder)) 92 | continue; 93 | $lastFolder = $lastFolder . $folder . '/'; 94 | if (!file_exists($lastFolder)) 95 | mkdir($lastFolder); 96 | } 97 | } 98 | 99 | /** 100 | * Creat JPG preview 101 | * @param $sourceImagePath 102 | * @throws ErrorException 103 | */ 104 | protected function createPreview($sourceImagePath, $watermarkInPng = null) 105 | { 106 | $img = new SimpleImage(); 107 | $img->load($sourceImagePath); 108 | 109 | if ($watermarkInPng) 110 | $img->watermark($watermarkInPng); 111 | 112 | $imgWidth = $img->getWidth(); 113 | $imgHeight = $img->getHeight(); 114 | 115 | if ($this->width && $this->width < $imgWidth) { 116 | $ratio = $this->width / $imgWidth; 117 | $img->resizeToWidth($this->width); 118 | } 119 | 120 | $saveType = $img->image_type; 121 | if ($saveType == IMG_WEBP || $saveType == IMG_QUADRATIC) { 122 | $saveType = IMG_JPEG; 123 | } 124 | 125 | $img->save($this->fileName, $saveType); 126 | } 127 | 128 | 129 | /** 130 | * Create webp from default preview 131 | */ 132 | protected function createPreviewWebp() 133 | { 134 | $img = new SimpleImage(); 135 | $img->load($this->fileName); 136 | $img->save($this->fileNameWebp, IMAGETYPE_WEBP, 70); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/logic/PathGenerator.php: -------------------------------------------------------------------------------- 1 | path = $path1 . DIRECTORY_SEPARATOR . md5(rand(0, 1000) . time()); 47 | } 48 | 49 | public function __toString() 50 | { 51 | return $this->path; 52 | } 53 | } -------------------------------------------------------------------------------- /src/logic/VideoFrameExtractor.php: -------------------------------------------------------------------------------- 1 | ffmpegBin = Yii::$app->getModule('files')->ffmpeg; 21 | 22 | if (!file_exists($this->ffmpegBin)) 23 | throw new ErrorException("ffmpeg is not found: {$this->ffmpegBin}"); 24 | 25 | if (!is_executable($this->ffmpegBin)) 26 | throw new ErrorException("ffmpeg is not executable: {$this->ffmpegBin}"); 27 | 28 | if (!is_file($videoSourceFilename)) 29 | throw new ErrorException('File not found on disk.'); 30 | 31 | $this->videoSourceFilename = $videoSourceFilename; 32 | $this->outputImageFilename = $outputImageFilename; 33 | } 34 | 35 | /** 36 | * @throws ErrorException 37 | */ 38 | public function extract() 39 | { 40 | $command = "{$this->ffmpegBin} -i {$this->videoSourceFilename} -ss 00:00:05.000 -vframes 1 {$this->outputImageFilename}"; 41 | exec($command, $out, $result); 42 | if (is_file($this->outputImageFilename)) { 43 | return true; 44 | } 45 | 46 | $command = "{$this->ffmpegBin} -i {$this->videoSourceFilename} -ss 00:00:03.000 -vframes 1 {$this->outputImageFilename}"; 47 | exec($command, $out, $result); 48 | if (is_file($this->outputImageFilename)) { 49 | return true; 50 | } 51 | 52 | 53 | $command = "{$this->ffmpegBin} -i {$this->videoSourceFilename} -ss 00:00:01.000 -vframes 1 {$this->outputImageFilename}"; 54 | exec($command, $out, $result); 55 | if (is_file($this->outputImageFilename)) { 56 | return true; 57 | } 58 | 59 | throw new ErrorException('FFmpeg frame extracting fail:' . print_r($out, true)); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/messages/ru/files.php: -------------------------------------------------------------------------------- 1 | 'Редактирование изображения', 11 | 'Cancel' => ' Отмена', 12 | 'Save' => 'Сохранить', 13 | 'Upload' => 'Загрузить', 14 | 'Filename' => 'Имя файла', 15 | 'View' => 'Просмотреть', 16 | 'Rename' => 'Переименовать', 17 | 'Delete all' => 'Удалить все', 18 | 'Delete' => 'Удалить', 19 | 'Edit' => 'Редактировать', 20 | 'Download' => 'Скачать', 21 | 'Download all' => 'Скачать все', 22 | 'The file is removed' => 'Файл удален', 23 | 'The files are removed' => 'Файлы удалены', 24 | 'The file is uploaded' => 'Файл загружен', 25 | 'The file is saved' => 'Файл сохранен', 26 | 'The file is renamed' => 'Файл переименован', 27 | 'Copy link to clipboard' => 'Копировать ссылку', 28 | 'queued' => 'в очереди', 29 | 'converting' => 'конвертируется', 30 | 'ready' => 'готово', 31 | 'file' => 'файл', 32 | 'image' => 'изображение', 33 | 'video' => 'видео', 34 | ]; -------------------------------------------------------------------------------- /src/migrations/m180627_121715_files.php: -------------------------------------------------------------------------------- 1 | db->driverName === 'mysql') { 12 | $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; 13 | } 14 | 15 | $this->createTable( 16 | '{{%file}}', 17 | [ 18 | 'id' => $this->primaryKey(11), 19 | 'class' => $this->string(255)->notNull(), 20 | 'field' => $this->string(255)->notNull(), 21 | 'object_id' => $this->integer(11)->notNull()->defaultValue(0), 22 | 'title' => $this->string(255)->notNull(), 23 | 'filename' => $this->string(255)->notNull(), 24 | 'content_type' => $this->string(255)->notNull(), 25 | 'type' => $this->integer(1)->notNull(), 26 | 'video_status' => $this->integer(1)->null()->defaultValue(null), 27 | 'ordering' => $this->integer(11)->notNull()->defaultValue(0), 28 | 'created' => $this->integer(11)->notNull(), 29 | 'user_id' => $this->integer(11)->null(), 30 | 'size' => $this->integer(20)->notNull(), 31 | 'hash' => $this->string(255)->null(), 32 | ], $tableOptions 33 | ); 34 | } 35 | 36 | public function safeDown() 37 | { 38 | 39 | $this->dropTable('{{%file}}'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/migrations/m190803_101632_alter_file.php: -------------------------------------------------------------------------------- 1 | createIndex('file-class', '{{%file}}', 'class'); 16 | $this->createIndex('file-object_id', '{{%file}}', 'object_id'); 17 | $this->createIndex('file-field', '{{%file}}', 'field'); 18 | $this->createIndex('file-type', '{{%file}}', 'type'); 19 | $this->createIndex('file-hash', '{{%file}}', 'hash'); 20 | $this->createIndex('file-filename', '{{%file}}', 'filename'); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function safeDown() 27 | { 28 | echo "m190803_101632_alter_file cannot be reverted.\n"; 29 | 30 | return false; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/migrations/m230602_185211_add_alt_to_file.php: -------------------------------------------------------------------------------- 1 | addColumn('file', 'alt', $this->string(512)->null()); 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function safeDown(): void 22 | { 23 | $this->dropColumn('file', 'alt'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/models/File.php: -------------------------------------------------------------------------------- 1 | getModule('files')->db; 51 | } 52 | 53 | /** 54 | * @inheritdoc 55 | */ 56 | 57 | public static function tableName() 58 | { 59 | return '{{%file}}'; 60 | } 61 | 62 | /** 63 | * Create hash if its empty 64 | * @param bool $insert 65 | * @return bool 66 | */ 67 | public function beforeSave($insert) 68 | { 69 | if (!$this->hash) { 70 | $this->changeHash(); 71 | } 72 | return parent::beforeSave($insert); 73 | } 74 | 75 | /** 76 | * Change object hash 77 | */ 78 | public function changeHash() 79 | { 80 | $this->hash = md5(time() . rand(99999, 99999999)); 81 | 82 | } 83 | 84 | /** 85 | * @return string 86 | */ 87 | public function getIcon() 88 | { 89 | $icon = IconHelper::FILE; 90 | 91 | if ($this->content_type == 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') 92 | $icon = IconHelper::FILE_WORD; 93 | 94 | if ($this->content_type == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') 95 | $icon = IconHelper::FILE_EXCEL; 96 | 97 | if ($this->content_type == 'application/vnd.openxmlformats-officedocument.presentationml.presentation') 98 | $icon = IconHelper::FILE_POWERPOINT; 99 | 100 | if ($this->content_type == 'application/x-zip-compressed') 101 | $icon = IconHelper::FILE_ARCHIVE; 102 | 103 | if ($this->content_type == 'application/octet-stream') 104 | $icon = IconHelper::FILE_ARCHIVE; 105 | 106 | if (preg_match('/audio/', $this->content_type)) 107 | $icon = IconHelper::FILE_AUDIO; 108 | 109 | if (preg_match('/pdf/', $this->content_type)) 110 | $icon = IconHelper::FILE_PDF; 111 | 112 | if ($this->type == FileType::VIDEO) 113 | $icon = IconHelper::FILE_VIDEO; 114 | 115 | return $icon; 116 | } 117 | 118 | function mime_content_type($filename) 119 | { 120 | $idx = explode('.', $filename); 121 | $count_explode = count($idx); 122 | $idx = strtolower($idx[$count_explode - 1]); 123 | 124 | $mimet = array( 125 | 'txt' => 'text/plain', 126 | 'htm' => 'text/html', 127 | 'html' => 'text/html', 128 | 'php' => 'text/html', 129 | 'css' => 'text/css', 130 | 'js' => 'application/javascript', 131 | 'json' => 'application/json', 132 | 'xml' => 'application/xml', 133 | 'swf' => 'application/x-shockwave-flash', 134 | 'flv' => 'video/x-flv', 135 | 136 | // images 137 | 'png' => 'image/png', 138 | 'jpe' => 'image/jpeg', 139 | 'jpeg' => 'image/jpeg', 140 | 'jpg' => 'image/jpeg', 141 | 'gif' => 'image/gif', 142 | 'bmp' => 'image/bmp', 143 | 'ico' => 'image/vnd.microsoft.icon', 144 | 'tiff' => 'image/tiff', 145 | 'tif' => 'image/tiff', 146 | 'svg' => 'image/svg+xml', 147 | 'svgz' => 'image/svg+xml', 148 | 149 | // archives 150 | 'zip' => 'application/zip', 151 | 'rar' => 'application/x-rar-compressed', 152 | 'exe' => 'application/x-msdownload', 153 | 'msi' => 'application/x-msdownload', 154 | 'cab' => 'application/vnd.ms-cab-compressed', 155 | 156 | // audio/video 157 | 'mp3' => 'audio/mpeg', 158 | 'qt' => 'video/quicktime', 159 | 'mov' => 'video/quicktime', 160 | 161 | // adobe 162 | 'pdf' => 'application/pdf', 163 | 'psd' => 'image/vnd.adobe.photoshop', 164 | 'ai' => 'application/postscript', 165 | 'eps' => 'application/postscript', 166 | 'ps' => 'application/postscript', 167 | 168 | // ms office 169 | 'doc' => 'application/msword', 170 | 'rtf' => 'application/rtf', 171 | 'xls' => 'application/vnd.ms-excel', 172 | 'ppt' => 'application/vnd.ms-powerpoint', 173 | 'docx' => 'application/msword', 174 | 'xlsx' => 'application/vnd.ms-excel', 175 | 'pptx' => 'application/vnd.ms-powerpoint', 176 | 177 | 178 | // open office 179 | 'odt' => 'application/vnd.oasis.opendocument.text', 180 | 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', 181 | ); 182 | 183 | if (isset($mimet[$idx])) { 184 | return $mimet[$idx]; 185 | } else { 186 | return 'application/octet-stream'; 187 | } 188 | } 189 | 190 | /** 191 | * @inheritdoc 192 | */ 193 | 194 | public function rules() 195 | { 196 | return [ 197 | [['class', 'field', 'filename', 'content_type', 'type'], 'required'], 198 | [['object_id', 'type', 'video_status', 'ordering'], 'integer'], 199 | [['class', 'field', 'title', 'filename', 'content_type', 'alt'], 'string', 'max' => 255], 200 | ]; 201 | } 202 | 203 | /** 204 | * @inheritdoc 205 | */ 206 | 207 | public function attributeLabels() 208 | { 209 | return [ 210 | 'id' => Yii::t('app', 'ID'), 211 | 'class' => Yii::t('app', 'Class'), 212 | 'field' => Yii::t('app', 'Field'), 213 | 'object_id' => Yii::t('app', 'Object ID'), 214 | 'title' => Yii::t('app', 'Title'), 215 | 'filename' => Yii::t('app', 'Filename'), 216 | 'content_type' => Yii::t('app', 'Con tent Type'), 217 | 'type' => Yii::t('app', 'Type'), 218 | 'video_status' => Yii::t('app', 'Video Status'), 219 | 'alt' => Yii::t('app', 'Alternative title'), 220 | ]; 221 | } 222 | 223 | /** 224 | * Return root path of preview 225 | * @return string 226 | */ 227 | 228 | public function getRootPreviewPath() 229 | { 230 | if ($this->isSvg()) 231 | return $this->getRootPath(); 232 | 233 | return Yii::$app->getModule('files')->storageFullPath . $this->filename . '.jpg'; 234 | } 235 | 236 | /** 237 | * @return bool 238 | */ 239 | public function isSvg() 240 | { 241 | return $this->content_type == 'image/svg+xml'; 242 | } 243 | 244 | /** 245 | * Return root path of image 246 | * @return string 247 | */ 248 | 249 | public function getRootPath() 250 | { 251 | return Yii::$app->getModule('files')->storageFullPath . DIRECTORY_SEPARATOR . $this->filename; 252 | } 253 | 254 | 255 | /** 256 | * Return web path 257 | * @return string 258 | */ 259 | 260 | public function getHref() 261 | { 262 | return Url::to(['/files/default/get', 'hash' => $this->hash]); 263 | } 264 | 265 | /** 266 | * @return bool 267 | */ 268 | public function isImage(): bool 269 | { 270 | return $this->type == FileType::IMAGE; 271 | } 272 | 273 | /** 274 | * Delete files from disk 275 | */ 276 | 277 | public function afterDelete() 278 | { 279 | $this->deleteFiles(); 280 | parent::afterDelete(); 281 | } 282 | 283 | /** 284 | * Method to read files from any mime types 285 | * @return bool 286 | */ 287 | 288 | // public function imageCreateFromAny() 289 | // { 290 | // $type = exif_imagetype($this->rootPath); 291 | // $allowedTypes = array( 292 | // 1, // [] gif 293 | // 2, // [] jpg 294 | // 3, // [] png 295 | // 6 // [] bmp 296 | // ); 297 | // if (!in_array($type, $allowedTypes)) { 298 | // return false; 299 | // } 300 | // switch ($type) { 301 | // case 1 : 302 | // $im = imageCreateFromGif($this->rootPath); 303 | // break; 304 | // case 2 : 305 | // $im = imageCreateFromJpeg($this->rootPath); 306 | // break; 307 | // case 3 : 308 | // $im = imageCreateFromPng($this->rootPath); 309 | // break; 310 | // case 6 : 311 | // $im = imageCreateFromBmp($this->rootPath); 312 | // break; 313 | // } 314 | // return $im; 315 | // } 316 | 317 | /** 318 | * Delete all files 319 | */ 320 | public function deleteFiles() 321 | { 322 | $extension = pathinfo($this->rootPath, PATHINFO_EXTENSION); 323 | array_map('unlink', glob(str_replace(".{$extension}", '*', $this->rootPath))); 324 | } 325 | 326 | /** 327 | * Set object_id to 0 to break link with object 328 | * @return void 329 | */ 330 | public function setZeroObject() 331 | { 332 | $this->object_id = 0; 333 | $this->save(false); 334 | } 335 | 336 | /** 337 | * @return mixed|null 338 | */ 339 | public function getWatermark() 340 | { 341 | $owner = new $this->class(); 342 | if ( 343 | isset($owner->behaviors['files']) && 344 | isset($owner->behaviors['files']->attributes[$this->field]) && 345 | isset($owner->behaviors['files']->attributes[$this->field]['watermark']) 346 | ) 347 | return $owner->behaviors['files']->attributes[$this->field]['watermark']; 348 | } 349 | 350 | /** 351 | * @return string 352 | */ 353 | public function __toString() 354 | { 355 | return $this->href; 356 | } 357 | 358 | /** 359 | * Return webp path to preview 360 | * @param int $width 361 | * @param bool $webp 362 | * @return string 363 | * @throws ErrorException 364 | */ 365 | public function getPreviewWebPath(int $width = 0, bool $webp = false) 366 | { 367 | if (!file_exists($this->getRootPath())) 368 | return null; 369 | 370 | if (!$this->isVideo() && !$this->isImage()) 371 | throw new ErrorException('Requiested file is not an image and its implsible to resize it.'); 372 | 373 | if (Yii::$app->getModule('files')->hostStatic) 374 | return 375 | Yii::$app->getModule('files')->hostStatic . 376 | $this->makeNameWithSize($this->filename, $width, $webp) . 377 | "?hash={$this->hash}&width={$width}&webp=" . intval($webp); 378 | 379 | return Url::toRoute(['/files/default/image', 'hash' => $this->hash, 'width' => $width, 'webp' => $webp]); 380 | } 381 | 382 | /** 383 | * @return bool 384 | */ 385 | public function isVideo(): bool 386 | { 387 | return $this->type == FileType::VIDEO; 388 | } 389 | 390 | /** 391 | * Creates file paths to file versions 392 | * @param $name 393 | * @param int $width 394 | * @param bool $webp 395 | * @return string 396 | */ 397 | public function makeNameWithSize($name, $width = 0, $webp = false) 398 | { 399 | $extension = pathinfo($this->rootPath, PATHINFO_EXTENSION); 400 | $rootPath = str_replace(".{$extension}", '', $name) . "_w" . $width . ".{$extension}"; 401 | return str_replace($extension, $webp ? 'webp' : 'jpeg', $rootPath); 402 | } 403 | 404 | /** 405 | * Returns full path to custom preview version 406 | * @param int $width 407 | * @param bool $webp 408 | * @return string 409 | * @throws ErrorException 410 | */ 411 | public function getPreviewRootPath($width = 0, $webp = false) 412 | { 413 | if (!$this->isVideo() && !$this->isImage()) 414 | throw new ErrorException('Requiested file is not an image and its implsible to resize it.'); 415 | return $this->makeNameWithSize($this->rootPath, $width, $webp); 416 | } 417 | 418 | /** 419 | * @return bool 420 | */ 421 | public function isFile(): bool 422 | { 423 | return $this->type == FileType::FILE; 424 | } 425 | 426 | } 427 | -------------------------------------------------------------------------------- /src/models/FileType.php: -------------------------------------------------------------------------------- 1 | 'file', 17 | self::IMAGE => 'image', 18 | self::VIDEO => 'video', 19 | ]; 20 | 21 | public static $messageCategory = 'files'; 22 | } -------------------------------------------------------------------------------- /src/models/VideoStatus.php: -------------------------------------------------------------------------------- 1 | 'queued', 19 | self::CONVERTING => 'converting', 20 | self::READY => 'ready', 21 | ]; 22 | 23 | } -------------------------------------------------------------------------------- /src/views/default/_cropper.php: -------------------------------------------------------------------------------- 1 | 12 | 13 |
    14 | 15 |
    16 | 17 | 18 | 36 | 37 | 55 | 56 | 104 | -------------------------------------------------------------------------------- /src/views/default/_file.php: -------------------------------------------------------------------------------- 1 | 31 | 32 |
    33 | 34 | 56 | 57 | 112 | 113 |
    114 | 115 | 116 | -------------------------------------------------------------------------------- /src/views/default/_single.php: -------------------------------------------------------------------------------- 1 | 33 |
    34 | 35 | type == FileType::IMAGE): ?> 36 | 43 | 44 | 45 | 46 | 61 | 62 | 63 | 106 | 107 |
    108 | 109 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | safeDown(); 27 | } 28 | 29 | /** 30 | * Настраиваем основные параметры приложения: базу данных и модуль 31 | */ 32 | 33 | protected function setApp() 34 | { 35 | $files = [ 36 | 'class' => 'floor12\files\Module', 37 | 'storage' => '@app/storage', 38 | ]; 39 | Yii::$app->setModule('files', $files); 40 | 41 | 42 | $db = [ 43 | 'class' => 'yii\db\Connection', 44 | 'dsn' => "sqlite:$this->sqlite", 45 | ]; 46 | Yii::$app->set('db', $db); 47 | 48 | $urlManager = [ 49 | 'class' => UrlManager::class, 50 | 'enablePrettyUrl' => true, 51 | 'showScriptName' => false, 52 | 'baseUrl' => 'http://test.com', 53 | ]; 54 | Yii::$app->set('urlManager', $urlManager); 55 | 56 | Yii::createObject(m180627_121715_files::class, [])->safeUp(); 57 | 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | protected function tearDown(): void 64 | { 65 | $this->destroyApplication(); 66 | parent::tearDown(); 67 | } 68 | 69 | /** 70 | * Убиваем приложение 71 | */ 72 | protected function destroyApplication() 73 | { 74 | Yii::$app = null; 75 | } 76 | 77 | /** 78 | * @inheritdoc 79 | */ 80 | public function setUp(): void 81 | { 82 | parent::setUp(); 83 | $this->mockApplication(); 84 | } 85 | 86 | /** 87 | * Запускаем приложение 88 | */ 89 | protected function mockApplication() 90 | { 91 | new Application([ 92 | 'id' => 'testapp', 93 | 'basePath' => __DIR__, 94 | 'vendorPath' => dirname(__DIR__) . '/vendor', 95 | 'runtimePath' => __DIR__ . '/runtime', 96 | ]); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | db->driverName === 'mysql') { 13 | $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB'; 14 | } 15 | 16 | $this->createTable( 17 | '{{%file}}', 18 | [ 19 | 'id' => $this->primaryKey(11), 20 | 'class' => $this->string(255)->notNull(), 21 | 'field' => $this->string(255)->notNull(), 22 | 'object_id' => $this->integer(11)->notNull()->defaultValue(0), 23 | 'title' => $this->string(255)->notNull(), 24 | 'filename' => $this->string(255)->notNull(), 25 | 'content_type' => $this->string(255)->notNull(), 26 | 'type' => $this->integer(1)->notNull(), 27 | 'video_status' => $this->integer(1)->null()->defaultValue(null), 28 | 'ordering' => $this->integer(11)->notNull()->defaultValue(0), 29 | 'created' => $this->integer(11)->notNull(), 30 | 'user_id' => $this->integer(11)->null(), 31 | 'size' => $this->integer(20)->notNull(), 32 | 'hash' => $this->string(255)->null(), 33 | ], $tableOptions 34 | ); 35 | } 36 | 37 | public function safeDown() 38 | { 39 | 40 | $this->dropTable('{{%file}}'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/data/photo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floor12/yii2-module-files/953d45bc911e0eee5df44864fd139e85925e95ec/tests/data/photo.jpeg -------------------------------------------------------------------------------- /tests/data/photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floor12/yii2-module-files/953d45bc911e0eee5df44864fd139e85925e95ec/tests/data/photo.png -------------------------------------------------------------------------------- /tests/data/testImage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floor12/yii2-module-files/953d45bc911e0eee5df44864fd139e85925e95ec/tests/data/testImage.jpg -------------------------------------------------------------------------------- /tests/logic/ClassnameEncoderTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('test\\\\class\\\\Name', $encoded); 25 | } 26 | 27 | /** 28 | * Проверяем обработку без слешей 29 | */ 30 | public function testEncodeClassName() 31 | { 32 | $testname = 'testname'; 33 | $encoded = (string)new ClassnameEncoder($testname); 34 | $this->assertEquals($encoded, $testname); 35 | } 36 | 37 | /** 38 | * Смотрим чтобы не было исключений и ошибок 39 | */ 40 | public function testEncodeEmptyClassName() 41 | { 42 | $testname = ''; 43 | $encoded = (string)new ClassnameEncoder($testname); 44 | $this->assertEquals($encoded, $testname); 45 | } 46 | 47 | 48 | } 49 | -------------------------------------------------------------------------------- /tests/logic/FileCreateFromInstanceTest.php: -------------------------------------------------------------------------------- 1 | setApp(); 25 | } 26 | 27 | public function tearDown(): void 28 | { 29 | $this->clearDb(); 30 | parent::tearDown(); 31 | } 32 | 33 | /** Вызываем без параметров 34 | * 35 | */ 36 | 37 | public function testNoParams() 38 | { 39 | $this->expectException(ArgumentCountError::class); 40 | new FileCreateFromInstance(); 41 | } 42 | 43 | /** Вызываем без параметров 44 | * 45 | * 46 | */ 47 | 48 | public function testBadParams() 49 | { 50 | $this->expectExceptionMessage("Attribute or class name not set."); 51 | $this->expectException(\yii\web\BadRequestHttpException::class); 52 | $instance = new UploadedFile(); 53 | $data = []; 54 | new FileCreateFromInstance($instance, $data); 55 | } 56 | 57 | /** Вызываем без параметров 58 | * 59 | * 60 | */ 61 | 62 | public function testWrongOwnerClassname() 63 | { 64 | $this->expectExceptionMessage("Attribute or class name not set."); 65 | $this->expectException(BadRequestHttpException::class); 66 | 67 | $instance = new UploadedFile(); 68 | $data = [ 69 | 'modelClass' => "notExistClassName", 70 | 'attribute' => 'images', 71 | ]; 72 | $logic = new FileCreateFromInstance($instance, [], null, false); 73 | } 74 | 75 | /** Вызываем с нормальными параметрами 76 | */ 77 | 78 | public function testGoodParams() 79 | { 80 | 81 | $instance = new UploadedFile(); 82 | $instance->error = 0; 83 | $instance->name = 'testName.jpg'; 84 | $instance->tempName = "tests/data/testImage.jpg"; 85 | $instance->size = filesize($instance->tempName); 86 | $instance->type = "image/jpeg"; 87 | 88 | 89 | $this->assertTrue(file_exists($instance->tempName)); 90 | 91 | $data = [ 92 | 'modelClass' => TestModel::class, 93 | 'attribute' => 'images', 94 | ]; 95 | $logic = new FileCreateFromInstance($instance, $data, null, false); 96 | 97 | $model = $logic->execute(); 98 | 99 | $this->assertTrue(is_object($model)); 100 | $this->assertFalse($model->isNewRecord); 101 | $this->assertTrue(file_exists($model->rootPath), $model->rootPath); 102 | $this->assertTrue(file_exists($model->rootPath), $model->getPreviewWebPath()); 103 | 104 | 105 | } 106 | 107 | 108 | } 109 | -------------------------------------------------------------------------------- /tests/logic/FileCreateFromPathTest.php: -------------------------------------------------------------------------------- 1 | expectExceptionMessage("File not found on disk."); 35 | $this->expectException(ErrorException::class); 36 | new FileCreateFromPath( 37 | new File(), 38 | "wrongTestFileName.png", 39 | $this->testOwnerClassName, 40 | $this->testOwnerFieldName, 41 | $this->storagePath, 42 | $this->testFileName 43 | ); 44 | } 45 | 46 | /** Вызываем несуществуюий файл 47 | * 48 | * 49 | */ 50 | 51 | public function testEmptyParams() 52 | { 53 | $this->expectExceptionMessage("Empty params not allowed."); 54 | $this->expectException(ErrorException::class); 55 | new FileCreateFromPath( 56 | new File(), 57 | "wrongTestFileName.png", 58 | "", 59 | $this->testOwnerFieldName, 60 | $this->storagePath, 61 | $this->testFileName 62 | ); 63 | } 64 | 65 | /** Пробуем дать несуществующий адрес адрес хранилища 66 | * 67 | * 68 | */ 69 | 70 | public function testWrongStorage() 71 | { 72 | $this->expectExceptionMessage("File storage not found on disk."); 73 | $this->expectException(ErrorException::class); 74 | new FileCreateFromPath( 75 | new File(), 76 | $this->testFilePath, 77 | $this->testOwnerClassName, 78 | $this->testOwnerFieldName, 79 | "wrongPath", 80 | $this->testFileName 81 | ); 82 | } 83 | 84 | 85 | /** Не записываемое хранилище 86 | * 87 | * 88 | */ 89 | 90 | public function testNotWritableStorage() 91 | { 92 | $this->expectExceptionMessage("File storage not found on disk."); 93 | $this->expectException(ErrorException::class); 94 | new FileCreateFromPath( 95 | new File(), 96 | $this->testFilePath, 97 | $this->testOwnerClassName, 98 | $this->testOwnerFieldName, 99 | $this->testFileName 100 | ); 101 | } 102 | 103 | 104 | /** 105 | * Нормальный сценарий который пока протестировать нормально не удается. 106 | */ 107 | 108 | public function testCreate() 109 | { 110 | $this->setApp(); 111 | 112 | $file = new File(); 113 | 114 | $logicObject = new FileCreateFromPath( 115 | $file, 116 | $this->testFilePath, 117 | $this->testOwnerClassName, 118 | $this->testOwnerFieldName, 119 | $this->storagePath, 120 | $this->testFileName 121 | ); 122 | $this->assertTrue(is_object($logicObject)); 123 | $this->assertTrue($logicObject->execute()); 124 | 125 | 126 | // Проверяем, что файл сохранился нормально. 127 | $this->assertFalse($file->isNewRecord); 128 | $this->assertTrue(is_integer($file->id)); 129 | $this->assertEquals($this->testOwnerClassName, $file->class); 130 | $this->assertEquals(FileType::IMAGE, $file->type); 131 | $this->assertEquals("image/jpeg", $file->content_type); 132 | $this->assertTrue(file_exists($file->rootPath), $file->rootPath); 133 | 134 | $this->clearDb(); 135 | } 136 | 137 | 138 | } 139 | -------------------------------------------------------------------------------- /tests/logic/FileRenameTest.php: -------------------------------------------------------------------------------- 1 | setApp(); 26 | } 27 | 28 | public function tearDown(): void 29 | { 30 | $this->clearDb(); 31 | parent::tearDown(); // TODO: Change the autogenerated stub 32 | } 33 | 34 | /** Пробуем не передать данные 35 | * 36 | */ 37 | public function testNoParams() 38 | { 39 | $this->expectException(ArgumentCountError::class); 40 | $model = new FileRename(); 41 | } 42 | 43 | /** Не передаем ID файла 44 | * 45 | * 46 | */ 47 | public function testEmptyId() 48 | { 49 | $this->expectExceptionMessage("ID of file is not set."); 50 | $this->expectException(yii\web\BadRequestHttpException::class); 51 | $data = []; 52 | $model = new FileRename($data); 53 | } 54 | 55 | /** Не передаем ID файла 56 | * 57 | * 58 | */ 59 | public function testEmptyTitle() 60 | { 61 | $this->expectExceptionMessage("Title of file is not set."); 62 | $this->expectException(yii\web\BadRequestHttpException::class); 63 | $data = ['id' => 1]; 64 | $model = new FileRename($data); 65 | } 66 | 67 | /** Передаем ID несуществующего файла 68 | * 69 | * 70 | */ 71 | public function testFileNotExitstInDb() 72 | { 73 | $this->expectExceptionMessage("File not found."); 74 | $this->expectException(yii\web\NotFoundHttpException::class); 75 | $data = ['id' => 100, 'title' => 'new name']; 76 | $model = new FileRename($data); 77 | } 78 | 79 | /** Создаем файл и пробуем его переименовать. 80 | */ 81 | public function testGoodRename() 82 | { 83 | $oldName = "Old name"; 84 | $newName = "New name"; 85 | 86 | $model = new File(); 87 | $model->title = $oldName; 88 | $model->class = 'testClassName'; 89 | $model->filename = 'testFileName/jpg'; 90 | $model->created = time(); 91 | $model->field = 'images'; 92 | $model->size = 50000; 93 | $model->user_id = 1; 94 | $model->content_type = 'image/jpeg'; 95 | $model->type = FileType::IMAGE; 96 | $model->save(); 97 | $this->assertFalse($model->isNewRecord); 98 | 99 | $data = [ 100 | 'id' => 1, 101 | 'title' => $newName, 102 | ]; 103 | $ret = Yii::createObject(FileRename::class, [$data])->execute(); 104 | $this->assertEquals($ret, $newName); 105 | 106 | $model->refresh(); 107 | $this->assertEquals($model->title, $newName); 108 | 109 | } 110 | 111 | 112 | } 113 | -------------------------------------------------------------------------------- /tests/logic/PathGeneratorTest.php: -------------------------------------------------------------------------------- 1 | expectException(ArgumentCountError::class); 33 | new PathGenerator(); 34 | } 35 | 36 | /** 37 | * Empty storage path check 38 | * 39 | * 40 | * 41 | */ 42 | public function testEmptyStoragePath() 43 | { 44 | $this->expectException(ErrorException::class); 45 | $this->expectExceptionMessage("Storage path not set for path generator."); 46 | new PathGenerator(""); 47 | } 48 | 49 | /** 50 | * Create storage path if it not exists 51 | */ 52 | public function testCreateStoragePath() 53 | { 54 | $path = 'tests/st1'; 55 | new PathGenerator($path); 56 | $this->assertTrue(file_exists($path)); 57 | exec("rm -f $path"); 58 | } 59 | 60 | /** 61 | * Create file path and check it exists 62 | */ 63 | public function testGeneratePath() 64 | { 65 | $path = (string)new PathGenerator($this->storagePath); 66 | $pre_path = substr($path, 0, 6); 67 | $fullPath = "{$this->storagePath}{$pre_path}"; 68 | $this->assertTrue(file_exists($fullPath), $fullPath); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /tests/storage/.gitignore: -------------------------------------------------------------------------------- 1 | * --------------------------------------------------------------------------------