├── docs ├── customization.md ├── index.md ├── update.md ├── installation.md ├── configuration.md ├── acl.md ├── integration.md └── events.md ├── .gitignore ├── CONTRIBUTING.md ├── src ├── Events │ ├── BeforeInitialization.php │ ├── DiskSelected.php │ ├── Deleted.php │ ├── Deleting.php │ ├── Download.php │ ├── Unzip.php │ ├── FileCreated.php │ ├── FileCreating.php │ ├── Paste.php │ ├── UnzipCreated.php │ ├── UnzipFailed.php │ ├── DirectoryCreated.php │ ├── DirectoryCreating.php │ ├── FileUpdate.php │ ├── Zip.php │ ├── ZipFailed.php │ ├── ZipCreated.php │ ├── Rename.php │ ├── FilesUploaded.php │ ├── FilesUploading.php │ └── FilesUploadFailed.php ├── Services │ ├── TransferService │ │ ├── TransferFactory.php │ │ ├── Transfer.php │ │ ├── LocalTransfer.php │ │ └── ExternalTransfer.php │ ├── ACLService │ │ ├── ConfigACLRepository.php │ │ ├── DBACLRepository.php │ │ ├── ACLRepository.php │ │ └── ACL.php │ ├── ConfigService │ │ ├── ConfigRepository.php │ │ └── DefaultConfigRepository.php │ └── Zip.php ├── Requests │ ├── CustomErrorMessage.php │ └── RequestValidator.php ├── Traits │ ├── CheckTrait.php │ ├── PathTrait.php │ └── ContentTrait.php ├── FileManagerServiceProvider.php ├── routes.php ├── Middleware │ └── FileManagerACL.php ├── Controllers │ └── FileManagerController.php └── FileManager.php ├── examples └── wysiwyg │ ├── ckeditor.blade.php │ ├── fm-button.blade.php │ ├── tinymce.blade.php │ └── summernote.blade.php ├── migrations └── 2019_02_06_174631_make_acl_rules_table.php ├── composer.json ├── LICENSE.md ├── resources ├── views │ ├── fmButton.blade.php │ ├── summernote.blade.php │ ├── tinymce.blade.php │ ├── ckeditor.blade.php │ └── tinymce5.blade.php └── assets │ └── css │ └── file-manager.css ├── README.md └── config └── file-manager.php /docs/customization.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | node_modules/ 3 | npm-debug.log 4 | .idea 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | > Contributions are welcome, and are accepted via pull requests. 4 | 5 | ## Pull requests 6 | 7 | Please ensure all pull requests are made against the `develop` branch on GitHub. 8 | -------------------------------------------------------------------------------- /src/Events/BeforeInitialization.php: -------------------------------------------------------------------------------- 1 | = 8.1 14 | * ext-zip - for zip and unzip functions 15 | * Laravel 9 or higher 16 | * GD Library or Imagick for [intervention/image-laravel](https://github.com/Intervention/image-laravel) 17 | * Bootstrap 5 and Bootstrap Icons v1.8.0 and higher 18 | -------------------------------------------------------------------------------- /src/Events/DiskSelected.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function disk() 27 | { 28 | return $this->disk; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Services/TransferService/TransferFactory.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | File manager and CKeditor 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Requests/CustomErrorMessage.php: -------------------------------------------------------------------------------- 1 | container->call([$this, 'message']) 19 | : 'The given data was invalid.'; 20 | 21 | throw new HttpResponseException(response()->json([ 22 | 'errors' => $validator->errors(), 23 | 'message' => $message, 24 | ], 422)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Services/ACLService/ConfigACLRepository.php: -------------------------------------------------------------------------------- 1 | getUserID()] ?? []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Events/Deleted.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 27 | $this->items = $items; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function disk() 34 | { 35 | return $this->disk; 36 | } 37 | 38 | /** 39 | * @return array 40 | */ 41 | public function items() 42 | { 43 | return $this->items; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Events/Deleting.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 27 | $this->items = $request->input('items'); 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function disk() 34 | { 35 | return $this->disk; 36 | } 37 | 38 | /** 39 | * @return array 40 | */ 41 | public function items() 42 | { 43 | return $this->items; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Events/Download.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 27 | $this->path = $request->input('path'); 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function disk() 34 | { 35 | return $this->disk; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function path() 42 | { 43 | return $this->path; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Services/ACLService/DBACLRepository.php: -------------------------------------------------------------------------------- 1 | where('user_id', $this->getUserID()) 31 | ->get(['disk', 'path', 'access']) 32 | ->map(function ($item) { 33 | return get_object_vars($item); 34 | }) 35 | ->all(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Services/ACLService/ACLRepository.php: -------------------------------------------------------------------------------- 1 | [ 25 | * "disk" => "public" 26 | * "path" => "music" 27 | * "access" => 0 28 | * ], 29 | * 1 => [ 30 | * "disk" => "public" 31 | * "path" => "images" 32 | * "access" => 1 33 | * ] 34 | * 35 | * OR [] - if no results for selected user 36 | * 37 | * @return array 38 | */ 39 | public function getRules(): array; 40 | } 41 | -------------------------------------------------------------------------------- /migrations/2019_02_06_174631_make_acl_rules_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->unsignedBigInteger('user_id')->nullable(); 19 | $table->string('disk'); 20 | $table->string('path'); 21 | $table->tinyInteger('access'); 22 | $table->timestamps(); 23 | 24 | $table->foreign('user_id')->references('id')->on('users'); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('acl_rules'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "alexusmai/laravel-file-manager", 3 | "description": "File manager for Laravel", 4 | "keywords": [ 5 | "laravel", 6 | "file", 7 | "manager" 8 | ], 9 | "authors": [ 10 | { 11 | "name": "Aleksandr Manekin", 12 | "email": "alexusmai@gmail.com", 13 | "role": "Developer" 14 | } 15 | ], 16 | "homepage": "https://github.com/alexusami/laravel-file-manager", 17 | "license": "MIT", 18 | "minimum-stability": "dev", 19 | "require": { 20 | "php": "^8.1", 21 | "ext-zip": "*", 22 | "ext-json": "*", 23 | "laravel/framework": "^9.0|^10.0|^11.0|^12.0", 24 | "league/flysystem": "^3.0", 25 | "intervention/image-laravel": "^1.2.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Alexusmai\\LaravelFileManager\\": "src" 30 | } 31 | }, 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "Alexusmai\\LaravelFileManager\\FileManagerServiceProvider" 36 | ] 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Events/Unzip.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 32 | $this->path = $request->input('path'); 33 | $this->folder = $request->input('folder'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function disk() 40 | { 41 | return $this->disk; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function path() 48 | { 49 | return $this->path; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function folder() 56 | { 57 | return $this->folder; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Events/FileCreated.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 32 | $this->path = $request->input('path'); 33 | $this->name = $request->input('name'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function disk() 40 | { 41 | return $this->disk; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function path() 48 | { 49 | return $this->path; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function name() 56 | { 57 | return $this->name; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Events/FileCreating.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 32 | $this->path = $request->input('path'); 33 | $this->name = $request->input('name'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function disk() 40 | { 41 | return $this->disk; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function path() 48 | { 49 | return $this->path; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function name() 56 | { 57 | return $this->name; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Events/Paste.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 32 | $this->path = $request->input('path'); 33 | $this->clipboard = $request->input('clipboard'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function disk() 40 | { 41 | return $this->disk; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function path() 48 | { 49 | return $this->path; 50 | } 51 | 52 | /** 53 | * @return array 54 | */ 55 | public function clipboard() 56 | { 57 | return $this->clipboard; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Events/UnzipCreated.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 32 | $this->path = $request->input('path'); 33 | $this->folder = $request->input('folder'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function disk() 40 | { 41 | return $this->disk; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function path() 48 | { 49 | return $this->path; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function folder() 56 | { 57 | return $this->folder; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Events/UnzipFailed.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 32 | $this->path = $request->input('path'); 33 | $this->folder = $request->input('folder'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function disk() 40 | { 41 | return $this->disk; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function path() 48 | { 49 | return $this->path; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function folder() 56 | { 57 | return $this->folder; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Events/DirectoryCreated.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 32 | $this->path = $request->input('path'); 33 | $this->name = $request->input('name'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function disk() 40 | { 41 | return $this->disk; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function path() 48 | { 49 | return $this->path; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function name() 56 | { 57 | return $this->name; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Aleksandr Manekin alexusmai@gmail.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/Events/DirectoryCreating.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 32 | $this->path = $request->input('path'); 33 | $this->name = $request->input('name'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function disk() 40 | { 41 | return $this->disk; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function path() 48 | { 49 | return $this->path; 50 | } 51 | 52 | /** 53 | * @return string 54 | */ 55 | public function name() 56 | { 57 | return $this->name; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Events/FileUpdate.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 32 | $this->path = $request->input('path'); 33 | $this->file = $request->file('file'); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function disk() 40 | { 41 | return $this->disk; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function path() 48 | { 49 | if ($this->path) { 50 | return $this->path.'/'.$this->file->getClientOriginalName(); 51 | } 52 | 53 | return $this->file->getClientOriginalName(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Traits/CheckTrait.php: -------------------------------------------------------------------------------- 1 | configRepository->getDiskList()) 20 | && array_key_exists($name, config('filesystems.disks')); 21 | } 22 | 23 | /** 24 | * Check Disk and Path 25 | * 26 | * @param $disk 27 | * @param $path 28 | * 29 | * @return bool 30 | */ 31 | public function checkPath($disk, $path): bool 32 | { 33 | // check disk name 34 | if (!$this->checkDisk($disk)) { 35 | return false; 36 | } 37 | 38 | // check path 39 | if ($path && !Storage::disk($disk)->exists($path)) { 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | /** 47 | * Disk/path not found message 48 | * 49 | * @return array 50 | */ 51 | public function notFoundMessage(): array 52 | { 53 | return [ 54 | 'result' => [ 55 | 'status' => 'danger', 56 | 'message' => 'notFound', 57 | ], 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Events/Zip.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 37 | $this->path = $request->input('path'); 38 | $this->name = $request->input('name'); 39 | $this->elements = $request->input('elements'); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function disk() 46 | { 47 | return $this->disk; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function path() 54 | { 55 | return $this->path; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function name() 62 | { 63 | return $this->name; 64 | } 65 | 66 | /** 67 | * @return array|string|null 68 | */ 69 | public function elements() 70 | { 71 | return $this->elements; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Events/ZipFailed.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 37 | $this->path = $request->input('path'); 38 | $this->name = $request->input('name'); 39 | $this->elements = $request->input('elements'); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function disk() 46 | { 47 | return $this->disk; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function path() 54 | { 55 | return $this->path; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function name() 62 | { 63 | return $this->name; 64 | } 65 | 66 | /** 67 | * @return array|string|null 68 | */ 69 | public function elements() 70 | { 71 | return $this->elements; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Events/ZipCreated.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 37 | $this->path = $request->input('path'); 38 | $this->name = $request->input('name'); 39 | $this->elements = $request->input('elements'); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function disk() 46 | { 47 | return $this->disk; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function path() 54 | { 55 | return $this->path; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function name() 62 | { 63 | return $this->name; 64 | } 65 | 66 | /** 67 | * @return array|string|null 68 | */ 69 | public function elements() 70 | { 71 | return $this->elements; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Traits/PathTrait.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ config('app.name', 'File Manager') }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /resources/views/summernote.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ config('app.name', 'File Manager') }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/wysiwyg/fm-button.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | File Manager and standalone button 12 | 13 | 14 | 15 |
16 |
17 |
18 | 19 |
20 | 22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 | 30 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Services/TransferService/Transfer.php: -------------------------------------------------------------------------------- 1 | disk = $disk; 21 | $this->path = $path; 22 | $this->clipboard = $clipboard; 23 | } 24 | 25 | /** 26 | * Transfer files and folders 27 | * 28 | * @return array 29 | */ 30 | public function filesTransfer(): array 31 | { 32 | try { 33 | // determine the type of operation 34 | if ($this->clipboard['type'] === 'copy') { 35 | $this->copy(); 36 | } elseif ($this->clipboard['type'] === 'cut') { 37 | $this->cut(); 38 | } 39 | } catch (\Exception $exception) { 40 | return [ 41 | 'result' => [ 42 | 'status' => 'error', 43 | 'message' => $exception->getMessage(), 44 | ], 45 | ]; 46 | } 47 | 48 | return [ 49 | 'result' => [ 50 | 'status' => 'success', 51 | 'message' => 'copied', 52 | ], 53 | ]; 54 | } 55 | 56 | abstract protected function copy(); 57 | 58 | abstract protected function cut(); 59 | } 60 | -------------------------------------------------------------------------------- /src/Events/Rename.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 37 | $this->newName = $request->input('newName'); 38 | $this->oldName = $request->input('oldName'); 39 | $this->type = $request->input('type'); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function disk() 46 | { 47 | return $this->disk; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function newName() 54 | { 55 | return $this->newName; 56 | } 57 | 58 | /** 59 | * @return string 60 | */ 61 | public function oldName() 62 | { 63 | return $this->oldName; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function type() 70 | { 71 | /* 72 | * $info = Storage::disk($this->disk)->getMetadata($this->oldName); 73 | * return $info['type']; 74 | */ 75 | 76 | return $this->type; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Events/FilesUploaded.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 37 | $this->path = $request->input('path'); 38 | $this->files = $request->file('files'); 39 | $this->overwrite = $request->input('overwrite'); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function disk() 46 | { 47 | return $this->disk; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function path() 54 | { 55 | return $this->path; 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public function files() 62 | { 63 | return array_map(function ($file) { 64 | return [ 65 | 'name' => $file->getClientOriginalName(), 66 | 'path' => $this->path.'/'.$file->getClientOriginalName(), 67 | 'extension' => $file->extension(), 68 | ]; 69 | }, $this->files); 70 | } 71 | 72 | /** 73 | * @return bool 74 | */ 75 | public function overwrite() 76 | { 77 | return !!$this->overwrite; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Events/FilesUploading.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 37 | $this->path = $request->input('path'); 38 | $this->files = $request->file('files'); 39 | $this->overwrite = $request->input('overwrite'); 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function disk() 46 | { 47 | return $this->disk; 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | public function path() 54 | { 55 | return $this->path; 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public function files() 62 | { 63 | return array_map(function ($file) { 64 | return [ 65 | 'name' => $file->getClientOriginalName(), 66 | 'path' => $this->path.'/'.$file->getClientOriginalName(), 67 | 'extension' => $file->extension(), 68 | ]; 69 | }, $this->files); 70 | } 71 | 72 | /** 73 | * @return bool 74 | */ 75 | public function overwrite() 76 | { 77 | return !!$this->overwrite; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /resources/views/tinymce.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ config('app.name', 'File Manager') }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /resources/views/ckeditor.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ config('app.name', 'File Manager') }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Requests/RequestValidator.php: -------------------------------------------------------------------------------- 1 | [ 34 | 'sometimes', 35 | 'string', 36 | function ($attribute, $value, $fail) use($config) { 37 | if (!in_array($value, $config->getDiskList()) || 38 | !array_key_exists($value, config('filesystems.disks')) 39 | ) { 40 | return $fail('diskNotFound'); 41 | } 42 | }, 43 | ], 44 | 'path' => [ 45 | 'sometimes', 46 | 'string', 47 | 'nullable', 48 | function ($attribute, $value, $fail) { 49 | if ($value && !Storage::disk($this->input('disk'))->exists($value) 50 | ) { 51 | return $fail('pathNotFound'); 52 | } 53 | }, 54 | ], 55 | ]; 56 | } 57 | 58 | /** 59 | * Not found message 60 | * 61 | * @return string 62 | */ 63 | public function message() 64 | { 65 | return 'notFound'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Events/FilesUploadFailed.php: -------------------------------------------------------------------------------- 1 | disk = $request->input('disk'); 39 | $this->path = $request->input('path'); 40 | $this->files = $request->file('files'); 41 | $this->overwrite = $request->input('overwrite'); 42 | $this->reason = $reason; 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function disk() 49 | { 50 | return $this->disk; 51 | } 52 | public function reason() 53 | { 54 | return $this->reason; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function path() 61 | { 62 | return $this->path; 63 | } 64 | 65 | /** 66 | * @return array 67 | */ 68 | public function files() 69 | { 70 | return array_map(function ($file) { 71 | return [ 72 | 'name' => $file->getClientOriginalName(), 73 | 'path' => $this->path.'/'.$file->getClientOriginalName(), 74 | 'extension' => $file->extension(), 75 | ]; 76 | }, $this->files); 77 | } 78 | 79 | /** 80 | * @return bool 81 | */ 82 | public function overwrite() 83 | { 84 | return !!$this->overwrite; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /examples/wysiwyg/tinymce.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | File manager and TinyMCE 12 | 13 | 14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 | 22 | 23 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /resources/views/tinymce5.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ config('app.name', 'File Manager') }} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /examples/wysiwyg/summernote.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | File Manager and SummerNote 14 | 15 | 16 | 17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/FileManagerServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadRoutesFrom(__DIR__.'/routes.php'); 21 | 22 | // views 23 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'file-manager'); 24 | 25 | // publish config 26 | $this->publishes([ 27 | __DIR__ 28 | .'/../config/file-manager.php' => config_path('file-manager.php'), 29 | ], 'fm-config'); 30 | 31 | // publish views 32 | $this->publishes([ 33 | __DIR__ 34 | .'/../resources/views' => resource_path('views/vendor/file-manager'), 35 | ], 'fm-views'); 36 | 37 | // publish js and css files - vue-file-manager module 38 | $this->publishes([ 39 | __DIR__ 40 | .'/../resources/assets' => public_path('vendor/file-manager'), 41 | ], 'fm-assets'); 42 | 43 | // publish migrations 44 | $this->publishes([ 45 | __DIR__ 46 | .'/../migrations' => database_path('migrations'), 47 | ], 'fm-migrations'); 48 | } 49 | 50 | /** 51 | * Register the application services. 52 | * 53 | * @return void 54 | */ 55 | public function register() 56 | { 57 | $this->mergeConfigFrom( 58 | __DIR__.'/../config/file-manager.php', 59 | 'file-manager' 60 | ); 61 | 62 | // Config Repository 63 | $this->app->bind( 64 | ConfigRepository::class, 65 | $this->app['config']['file-manager.configRepository'] 66 | ); 67 | 68 | // ACL Repository 69 | $this->app->bind( 70 | ACLRepository::class, 71 | $this->app->make(ConfigRepository::class)->getAclRepository() 72 | ); 73 | 74 | // register ACL middleware 75 | $this->app['router']->aliasMiddleware('fm-acl', FileManagerACL::class); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Services/ACLService/ACL.php: -------------------------------------------------------------------------------- 1 | aclRepository = $aclRepository; 32 | $this->configRepository = $configRepository; 33 | } 34 | 35 | /** 36 | * Get access level for selected path 37 | * 38 | * @param $disk 39 | * @param string $path 40 | * 41 | * @return int 42 | */ 43 | public function getAccessLevel($disk, $path = '/'): int 44 | { 45 | $rules = $this->rulesForDisk($disk); 46 | 47 | // find the first rule where the paths are equal 48 | $firstRule = Arr::first($rules, function ($value) use ($path) { 49 | return fnmatch($value['path'], $path); 50 | }); 51 | 52 | if ($firstRule) { 53 | return $firstRule['access']; 54 | } 55 | 56 | // blacklist or whitelist (ACL strategy) 57 | return $this->configRepository->getAclStrategy() === 'blacklist' ? 2 : 0; 58 | } 59 | 60 | /** 61 | * Select rules for disk 62 | * 63 | * @param $disk 64 | * 65 | * @return array 66 | */ 67 | protected function rulesForDisk($disk): array 68 | { 69 | return Arr::where($this->rulesList(), 70 | function ($value) use ($disk) { 71 | return $value['disk'] === $disk; 72 | }); 73 | } 74 | 75 | /** 76 | * Get rules list from ACL Repository 77 | * 78 | * @return array|mixed 79 | */ 80 | protected function rulesList(): mixed 81 | { 82 | // if cache on 83 | if ($minutes = $this->configRepository->getAclRulesCache()) { 84 | $cacheName = get_class($this->aclRepository) . '_' .$this->aclRepository->getUserID(); 85 | 86 | return Cache::remember($cacheName, $minutes, function () { 87 | return $this->aclRepository->getRules(); 88 | }); 89 | } 90 | 91 | return $this->aclRepository->getRules(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | 1. Install package - using composer 4 | 5 | ``` 6 | composer require alexusmai/laravel-file-manager 7 | ``` 8 | 9 | For Laravel 5 - 8 use v2.5.4 10 | 11 | ``` 12 | composer require alexusmai/laravel-file-manager "2.5.4" 13 | ``` 14 | 15 | 2. If you use Laravel 5.4, then add service provider to config/app.php (for the Laravel 5.5 and higher skip this step): 16 | 17 | ```php 18 | Alexusmai\LaravelFileManager\FileManagerServiceProvider::class, 19 | ``` 20 | 21 | 3. Publish configuration file 22 | 23 | ```bash 24 | php artisan vendor:publish --tag=fm-config 25 | ``` 26 | 27 | 4. You can install npm package directly and use it in your vue application - more information about it - 28 | [vue-laravel-file-manager](https://github.com/alexusmai/vue-laravel-file-manager) 29 | 30 | > OR 31 | 32 | Publish compiled and minimized js and css files 33 | 34 | ``` 35 | php artisan vendor:publish --tag=fm-assets 36 | ``` 37 | 38 | Open the view file where you want to place the application block, and add: 39 | 40 | * add a csrf token to head block if you did not do it before 41 | 42 | ``` 43 | 44 | 45 | ``` 46 | 47 | * For version 3 and higher - the frontend package uses **Bootstrap 5** and **Bootstrap Icons** styles, if you already use it, 48 | then you do not need to connect any styles. Otherwise, add - 49 | 50 | ``` 51 | 52 | 53 | ``` 54 | 55 | * For old versions - the frontend package uses **Bootstrap 4** and **Font Awesome 5** styles, if you already use it, 56 | then you do not need to connect any styles. Otherwise, add - 57 | 58 | ``` 59 | 60 | 61 | ``` 62 | 63 | * add file manager styles 64 | 65 | ``` 66 | 67 | ``` 68 | 69 | * add file manager js 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | * For version 3 and higher - add div for application (set application height!) 76 | 77 | ``` 78 |
79 | ``` 80 | 81 | * For old versions - add div for application (set application height!) 82 | 83 | ``` 84 |
85 |
86 |
87 | ``` 88 | 89 | ## What's next 90 | 91 | [Configuration](./configuration.md) 92 | -------------------------------------------------------------------------------- /src/Services/TransferService/LocalTransfer.php: -------------------------------------------------------------------------------- 1 | clipboard['files'] as $file) { 31 | Storage::disk($this->disk)->copy( 32 | $file, 33 | $this->renamePath($file, $this->path) 34 | ); 35 | } 36 | 37 | // directories 38 | foreach ($this->clipboard['directories'] as $directory) { 39 | $this->copyDirectory($directory); 40 | } 41 | } 42 | 43 | /** 44 | * Cut files and folders 45 | */ 46 | protected function cut() 47 | { 48 | // files 49 | foreach ($this->clipboard['files'] as $file) { 50 | Storage::disk($this->disk)->move( 51 | $file, 52 | $this->renamePath($file, $this->path) 53 | ); 54 | } 55 | 56 | // directories 57 | foreach ($this->clipboard['directories'] as $directory) { 58 | Storage::disk($this->disk)->move( 59 | $directory, 60 | $this->renamePath($directory, $this->path) 61 | ); 62 | } 63 | } 64 | 65 | /** 66 | * Copy directory 67 | * 68 | * @param $directory 69 | */ 70 | protected function copyDirectory($directory) 71 | { 72 | // get all directories in this directory 73 | $allDirectories = Storage::disk($this->disk) 74 | ->allDirectories($directory); 75 | 76 | $partsForRemove = count(explode('/', $directory)) - 1; 77 | 78 | // create this directories 79 | foreach ($allDirectories as $dir) { 80 | Storage::disk($this->disk)->makeDirectory( 81 | $this->transformPath( 82 | $dir, 83 | $this->path, 84 | $partsForRemove 85 | ) 86 | ); 87 | } 88 | 89 | // get all files 90 | $allFiles = Storage::disk($this->disk)->allFiles($directory); 91 | 92 | // copy files 93 | foreach ($allFiles as $file) { 94 | Storage::disk($this->disk)->copy( 95 | $file, 96 | $this->transformPath($file, $this->path, $partsForRemove) 97 | ); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/routes.php: -------------------------------------------------------------------------------- 1 | getMiddleware(); 11 | 12 | /** 13 | * If ACL ON add "fm-acl" middleware to array 14 | */ 15 | if ($config->getAcl()) { 16 | $middleware[] = 'fm-acl'; 17 | } 18 | 19 | Route::group([ 20 | 'middleware' => $middleware, 21 | 'prefix' => $config->getRoutePrefix(), 22 | 'namespace' => 'Alexusmai\LaravelFileManager\Controllers', 23 | ], function () { 24 | 25 | Route::get('initialize', [FileManagerController::class, 'initialize']) 26 | ->name('fm.initialize'); 27 | 28 | Route::get('content', [FileManagerController::class, 'content']) 29 | ->name('fm.content'); 30 | 31 | Route::get('tree', [FileManagerController::class, 'tree']) 32 | ->name('fm.tree'); 33 | 34 | Route::get('select-disk', [FileManagerController::class, 'selectDisk']) 35 | ->name('fm.select-disk'); 36 | 37 | Route::post('upload', [FileManagerController::class, 'upload']) 38 | ->name('fm.upload'); 39 | 40 | Route::post('delete', [FileManagerController::class, 'delete']) 41 | ->name('fm.delete'); 42 | 43 | Route::post('paste', [FileManagerController::class, 'paste']) 44 | ->name('fm.paste'); 45 | 46 | Route::post('rename', [FileManagerController::class, 'rename']) 47 | ->name('fm.rename'); 48 | 49 | Route::get('download', [FileManagerController::class, 'download']) 50 | ->name('fm.download'); 51 | 52 | Route::get('thumbnails', [FileManagerController::class, 'thumbnails']) 53 | ->name('fm.thumbnails'); 54 | 55 | Route::get('preview', [FileManagerController::class, 'preview']) 56 | ->name('fm.preview'); 57 | 58 | Route::get('url', [FileManagerController::class, 'url']) 59 | ->name('fm.url'); 60 | 61 | Route::post('create-directory', [FileManagerController::class, 'createDirectory']) 62 | ->name('fm.create-directory'); 63 | 64 | Route::post('create-file', [FileManagerController::class, 'createFile']) 65 | ->name('fm.create-file'); 66 | 67 | Route::post('update-file', [FileManagerController::class, 'updateFile']) 68 | ->name('fm.update-file'); 69 | 70 | Route::get('stream-file', [FileManagerController::class, 'streamFile']) 71 | ->name('fm.stream-file'); 72 | 73 | Route::post('zip', [FileManagerController::class, 'zip']) 74 | ->name('fm.zip'); 75 | 76 | Route::post('unzip', [FileManagerController::class, 'unzip']) 77 | ->name('fm.unzip'); 78 | 79 | // Route::get('properties', 'FileManagerController@properties'); 80 | 81 | // Integration with editors 82 | Route::get('ckeditor', [FileManagerController::class, 'ckeditor']) 83 | ->name('fm.ckeditor'); 84 | 85 | Route::get('tinymce', [FileManagerController::class, 'tinymce']) 86 | ->name('fm.tinymce'); 87 | 88 | Route::get('tinymce5', [FileManagerController::class, 'tinymce5']) 89 | ->name('fm.tinymce5'); 90 | 91 | Route::get('summernote', [FileManagerController::class, 'summernote']) 92 | ->name('fm.summernote'); 93 | 94 | Route::get('fm-button', [FileManagerController::class, 'fmButton']) 95 | ->name('fm.fm-button'); 96 | }); 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel File Manager 2 | 3 | [![Latest Stable Version](http://poser.pugx.org/alexusmai/laravel-file-manager/v)](https://packagist.org/packages/alexusmai/laravel-file-manager) 4 | [![Total Downloads](http://poser.pugx.org/alexusmai/laravel-file-manager/downloads)](https://packagist.org/packages/alexusmai/laravel-file-manager) 5 | [![Latest Unstable Version](http://poser.pugx.org/alexusmai/laravel-file-manager/v/unstable)](https://packagist.org/packages/alexusmai/laravel-file-manager) 6 | [![License](http://poser.pugx.org/alexusmai/laravel-file-manager/license)](https://packagist.org/packages/alexusmai/laravel-file-manager) 7 | [![PHP Version Require](http://poser.pugx.org/alexusmai/laravel-file-manager/require/php)](https://packagist.org/packages/alexusmai/laravel-file-manager) 8 | 9 | ![Laravel File Manager](https://raw.github.com/alexusmai/vue-laravel-file-manager/master/src/assets/laravel-file-manager.gif?raw=true) 10 | 11 | **Vue.js Frontend:** [alexusmai/vue-laravel-file-manager](https://github.com/alexusmai/vue-laravel-file-manager) 12 | 13 | ## Documentation 14 | 15 | [Laravel File Manager Docs](./docs/index.md) 16 | * [Installation](./docs/installation.md) 17 | * [Configuration](./docs/configuration.md) 18 | * [Integration](./docs/integration.md) 19 | * [ACL](./docs/acl.md) 20 | * [Events](./docs/events.md) 21 | * [Update](./docs/update.md) 22 | 23 | ## Features 24 | 25 | * Frontend on Vue.js - [vue-laravel-file-manager](https://github.com/alexusmai/vue-laravel-file-manager) 26 | * Work with the file system is organized by the standard means Laravel Flysystem: 27 | * Local, (S)FTP, S3, Dropbox ... 28 | * The ability to work only with the selected disks 29 | * Several options for displaying the file manager: 30 | * One-panel view 31 | * One-panel + Directory tree 32 | * Two-panel 33 | * The minimum required set of operations: 34 | * Creating files 35 | * Creating folders 36 | * Copying / Cutting Folders and Files 37 | * Renaming 38 | * Uploading files (multi-upload) 39 | * Downloading files 40 | * Two modes of displaying elements - table and grid 41 | * Preview for images 42 | * Viewing images 43 | * Full screen mode 44 | * More operations (v.2): 45 | * Audio player (mp3, ogg, wav, aac), Video player (webm, mp4) - ([Plyr](https://github.com/sampotts/plyr)) 46 | * Code editor - ([Code Mirror](https://github.com/codemirror/codemirror)) 47 | * Image cropper - ([Cropper.js](https://github.com/fengyuanchen/cropperjs)) 48 | * Zip / Unzip - only for local disks 49 | * Integration with WYSIWYG Editors: 50 | * CKEditor 4 51 | * TinyMCE 4 52 | * TinyMCE 5 53 | * SummerNote 54 | * Standalone button 55 | * ACL - access control list 56 | * delimiting access to files and folders 57 | * two work strategies: 58 | * blacklist - Allow everything that is not forbidden by the ACL rules list 59 | * whitelist - Deny everything, that not allowed by the ACL rules list 60 | * You can use different repositories for the rules - an array (configuration file), a database (there is an example implementation), or you can add your own. 61 | * You can hide files and folders that are not accessible. 62 | * Events (v2.2) 63 | * Thumbnails lazy load 64 | * Dynamic configuration (v2.4) 65 | * Supported locales : ru, en, ar, sr, cs, de, es, nl, zh-CN, fa, it, tr, fr, pt-BR, zh-TW, pl 66 | 67 | ## In a new version 3 68 | 69 | - **Version 3 only works with Laravel 9, 10 and 11!** 70 | - Vue.js 3 71 | - Bootstrap 5 72 | - Bootstrap Icons 73 | -------------------------------------------------------------------------------- /src/Services/ConfigService/ConfigRepository.php: -------------------------------------------------------------------------------- 1 | ['web', 'auth', 'admin'] 101 | * !!!! RESTRICT ACCESS FOR NON ADMIN USERS !!!! 102 | * 103 | * @return array 104 | */ 105 | public function getMiddleware(): array; 106 | 107 | /** 108 | * ACL mechanism ON/OFF 109 | * 110 | * default - false(OFF) 111 | * 112 | * @return bool 113 | */ 114 | public function getAcl(): bool; 115 | 116 | /** 117 | * Hide files and folders from file-manager if user doesn't have access 118 | * 119 | * ACL access level = 0 120 | * 121 | * @return bool 122 | */ 123 | public function getAclHideFromFM(): bool; 124 | 125 | /** 126 | * ACL strategy 127 | * 128 | * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list 129 | * 130 | * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list 131 | * 132 | * @return string 133 | */ 134 | public function getAclStrategy(): string; 135 | 136 | /** 137 | * ACL rules repository 138 | * 139 | * default - config file(ConfigACLRepository) 140 | * 141 | * @return string 142 | */ 143 | public function getAclRepository(): string; 144 | 145 | /** 146 | * ACL Rules cache 147 | * 148 | * null or value in minutes 149 | * 150 | * @return int|null 151 | */ 152 | public function getAclRulesCache(): ?int; 153 | 154 | /** 155 | * Whether to slugify filenames 156 | * 157 | * boolean 158 | * 159 | * @return bool|null 160 | */ 161 | public function getSlugifyNames(): ?bool; 162 | } 163 | -------------------------------------------------------------------------------- /src/Services/TransferService/ExternalTransfer.php: -------------------------------------------------------------------------------- 1 | manager = new MountManager([ 31 | 'from' => Storage::drive($clipboard['disk'])->getDriver(), 32 | 'to' => Storage::drive($disk)->getDriver(), 33 | ]); 34 | } 35 | 36 | /** 37 | * Copy files and folders 38 | * 39 | * @return void 40 | * @throws FilesystemException 41 | */ 42 | protected function copy() 43 | { 44 | // files 45 | foreach ($this->clipboard['files'] as $file) { 46 | $this->copyToDisk( 47 | $file, 48 | $this->renamePath($file, $this->path) 49 | ); 50 | } 51 | 52 | // directories 53 | foreach ($this->clipboard['directories'] as $directory) { 54 | $this->copyDirectoryToDisk($directory); 55 | } 56 | } 57 | 58 | /** 59 | * Cut files and folders 60 | * 61 | * @return void 62 | * @throws FilesystemException 63 | */ 64 | protected function cut() 65 | { 66 | // files 67 | foreach ($this->clipboard['files'] as $file) { 68 | $this->moveToDisk( 69 | $file, 70 | $this->renamePath($file, $this->path) 71 | ); 72 | } 73 | 74 | // directories 75 | foreach ($this->clipboard['directories'] as $directory) { 76 | $this->copyDirectoryToDisk($directory); 77 | 78 | // remove directory 79 | Storage::disk($this->clipboard['disk']) 80 | ->deleteDirectory($directory); 81 | } 82 | } 83 | 84 | /** 85 | * Copy directory to another disk 86 | * 87 | * @param $directory 88 | * 89 | * @return void 90 | * @throws FilesystemException 91 | */ 92 | protected function copyDirectoryToDisk($directory) 93 | { 94 | // get all directories in this directory 95 | $allDirectories = Storage::disk($this->clipboard['disk']) 96 | ->allDirectories($directory); 97 | 98 | $partsForRemove = count(explode('/', $directory)) - 1; 99 | 100 | // create this directories 101 | foreach ($allDirectories as $dir) { 102 | Storage::disk($this->disk)->makeDirectory( 103 | $this->transformPath($dir, $this->path, $partsForRemove) 104 | ); 105 | } 106 | 107 | // get all files 108 | $allFiles = Storage::disk($this->clipboard['disk']) 109 | ->allFiles($directory); 110 | 111 | // copy files 112 | foreach ($allFiles as $file) { 113 | $this->copyToDisk($file, 114 | $this->transformPath($file, $this->path, $partsForRemove)); 115 | } 116 | } 117 | 118 | /** 119 | * Copy files to disk 120 | * 121 | * @param $filePath 122 | * @param $newPath 123 | * 124 | * @return void 125 | * @throws FilesystemException 126 | */ 127 | protected function copyToDisk($filePath, $newPath) 128 | { 129 | $this->manager->copy( 130 | 'from://'.$filePath, 131 | 'to://'.$newPath 132 | ); 133 | } 134 | 135 | /** 136 | * Move files to disk 137 | * 138 | * @param $filePath 139 | * @param $newPath 140 | * 141 | * @return void 142 | * @throws FilesystemException 143 | */ 144 | protected function moveToDisk($filePath, $newPath) 145 | { 146 | $this->manager->move( 147 | 'from://'.$filePath, 148 | 'to://'.$newPath 149 | ); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Open configuration file - config/file-manager.php 4 | 5 | - fill the disk list from config/filesystem.php (select the desired drive names) 6 | - set cache 7 | - select file manager windows configuration 8 | 9 | **!!! Be sure to add your middleware to restrict access to the application !!!** 10 | 11 | **Don't forget to configure your php and Nginx** 12 | 13 | ``` 14 | // PHP 15 | upload_max_filesize, 16 | post_max_size 17 | 18 | // Nginx 19 | client_max_body_size 20 | ``` 21 | 22 | ### You can set default disk and default path 23 | 24 | You have two variants for how to do it: 25 | 26 | 1. Add this params to the config file (config/file-manager.php) 27 | 28 | ```php 29 | /** 30 | * Default disk for left manager 31 | * null - auto select the first disk in the disk list 32 | */ 33 | 'leftDisk' => 'public', 34 | 35 | /** 36 | * Default disk for right manager 37 | * null - auto select the first disk in the disk list 38 | */ 39 | 'rightDisk' => null, 40 | 41 | /** 42 | * Default path for left manager 43 | * null - root directory 44 | */ 45 | 'leftPath' => 'directory/sub-directory', 46 | 47 | /** 48 | * Default path for right manager 49 | * null - root directory 50 | */ 51 | 'rightPath' => null, 52 | ``` 53 | 54 | 2 Or you can add this params in URL 55 | 56 | ``` 57 | http://site.name/?leftDisk=public 58 | 59 | http://site.name/?leftDisk=public&rightDisk=images 60 | 61 | http://site.name/?leftDisk=public&leftPath=directory/sub-directory 62 | 63 | http://site.name/?leftDisk=public&leftPath=directory2&rightDisk=images&rightPath=cars/vw/golf 64 | // %2F - /, %20 - space 65 | http://site.name/?leftDisk=public&leftPath=directory2&rightDisk=images&rightPath=cars%2Fvw%2Fgolf 66 | ``` 67 | 68 | leftDisk and leftPath is default for the file manager windows configuration - 1,2 69 | 70 | **You can't add a disk that does not exist in the diskList array !** 71 | 72 | **! Params in URL have more weight than params in config file. It means that URL params can overwrite your config params. !** 73 | 74 | ## Disk settings example 75 | 76 | - config/filesystems.php 77 | 78 | ```php 79 | // Filesystem Disks 80 | 'disks' => [ 81 | // images folder in public path 82 | 'images' => [ 83 | 'driver' => 'local', 84 | 'root' => public_path('images'), 85 | 'url' => env('APP_URL').'/images', 86 | ], 87 | 88 | // public folder in storage/app/public 89 | 'public' => [ 90 | 'driver' => 'local', 91 | 'root' => storage_path('app/public'), 92 | 'url' => env('APP_URL').'/storage', // https://laravel.com/docs/5.7/filesystem#file-urls 93 | 'visibility' => 'public', 94 | ], 95 | 96 | // ftp 97 | 'dd-wrt' => [ 98 | 'driver' => 'ftp', 99 | 'host' => 'ftp.dd-wrt.com', 100 | 'username' => 'anonymous', 101 | 'passive' => true, 102 | 'timeout' => 30, 103 | ], 104 | ], 105 | ``` 106 | 107 | - config/file-manager.php 108 | 109 | ```php 110 | // You need to enter the disks you want to use in the file manager 111 | 'diskList' => ['images', 'public'], 112 | ``` 113 | 114 | ## Normalization of uploaded file names 115 | 116 | If you expect to work with files that may have filenames that are not considered *normal* `f.e.: "DCIM_2021 - čšč& (1).jpg.jpg"` so basically any time you give the option to upload files to users, you can set `slugifyNames` to `true` and have the names ran through `Str::slug()` before saving it so you file will look something like `dcim-2021-csc-1.jpg` 117 | 118 | ## Dynamic configuration 119 | 120 | You can create your own configuration, for example for different users or their roles. 121 | 122 | Create new class - example - TestConfigRepository 123 | 124 | ```php 125 | namespace App\Http; 126 | 127 | use Alexusmai\LaravelFileManager\Services\ConfigService\ConfigRepository; 128 | 129 | class TestConfigRepository implements ConfigRepository 130 | { 131 | // implement all methods from interface 132 | 133 | /** 134 | * Get disk list 135 | * 136 | * ['public', 'local', 's3'] 137 | * 138 | * @return array 139 | */ 140 | public function getDiskList(): array 141 | { 142 | if (\Auth::id() === 1) { 143 | return [ 144 | ['public', 'local', 's3'], 145 | ]; 146 | } 147 | 148 | return ['public']; 149 | } 150 | 151 | ... 152 | } 153 | ``` 154 | 155 | For example see [src/Services/ConfigService/DefaultConfigRepository.php](https://github.com/alexusmai/laravel-file-manager/blob/master/src/Services/ConfigService/DefaultConfigRepository.php) 156 | 157 | ## What's next 158 | 159 | [ACL](./acl.md) 160 | 161 | [Integration](./integration.md) 162 | -------------------------------------------------------------------------------- /config/file-manager.php: -------------------------------------------------------------------------------- 1 | DefaultConfigRepository::class, 14 | 15 | /** 16 | * ACL rules repository 17 | * 18 | * Default - ConfigACLRepository (see rules in - aclRules) 19 | */ 20 | 'aclRepository' => ConfigACLRepository::class, 21 | 22 | //********* Default configuration for DefaultConfigRepository ************** 23 | 24 | /** 25 | * LFM Route prefix 26 | * !!! WARNING - if you change it, you should compile frontend with new prefix(baseUrl) !!! 27 | */ 28 | 'routePrefix' => 'file-manager', 29 | 30 | /** 31 | * List of disk names that you want to use 32 | * (from config/filesystems) 33 | */ 34 | 'diskList' => ['public'], 35 | 36 | /** 37 | * Default disk for left manager 38 | * 39 | * null - auto select the first disk in the disk list 40 | */ 41 | 'leftDisk' => null, 42 | 43 | /** 44 | * Default disk for right manager 45 | * 46 | * null - auto select the first disk in the disk list 47 | */ 48 | 'rightDisk' => null, 49 | 50 | /** 51 | * Default path for left manager 52 | * 53 | * null - root directory 54 | */ 55 | 'leftPath' => null, 56 | 57 | /** 58 | * Default path for right manager 59 | * 60 | * null - root directory 61 | */ 62 | 'rightPath' => null, 63 | 64 | /** 65 | * File manager modules configuration 66 | * 67 | * 1 - only one file manager window 68 | * 2 - one file manager window with directories tree module 69 | * 3 - two file manager windows 70 | */ 71 | 'windowsConfig' => 2, 72 | 73 | /** 74 | * File upload - Max file size in KB 75 | * 76 | * null - no restrictions 77 | */ 78 | 'maxUploadFileSize' => null, 79 | 80 | /** 81 | * File upload - Allow these file types 82 | * 83 | * [] - no restrictions 84 | */ 85 | 'allowFileTypes' => [], 86 | 87 | /** 88 | * Show / Hide system files and folders 89 | */ 90 | 'hiddenFiles' => true, 91 | 92 | /*************************************************************************** 93 | * Middleware 94 | * 95 | * Add your middleware name to array -> ['web', 'auth', 'admin'] 96 | * !!!! RESTRICT ACCESS FOR NON ADMIN USERS !!!! 97 | */ 98 | 'middleware' => ['web'], 99 | 100 | /*************************************************************************** 101 | * ACL mechanism ON/OFF 102 | * 103 | * default - false(OFF) 104 | */ 105 | 'acl' => false, 106 | 107 | /** 108 | * Hide files and folders from file-manager if user doesn't have access 109 | * 110 | * ACL access level = 0 111 | */ 112 | 'aclHideFromFM' => true, 113 | 114 | /** 115 | * ACL strategy 116 | * 117 | * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list 118 | * 119 | * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list 120 | */ 121 | 'aclStrategy' => 'blacklist', 122 | 123 | /** 124 | * ACL Rules cache 125 | * 126 | * null or value in minutes 127 | */ 128 | 'aclRulesCache' => null, 129 | 130 | //********* Default configuration for DefaultConfigRepository END ********** 131 | 132 | 133 | /*************************************************************************** 134 | * ACL rules list - used for default ACL repository (ConfigACLRepository) 135 | * 136 | * 1 it's user ID 137 | * null - for not authenticated user 138 | * 139 | * 'disk' => 'disk-name' 140 | * 141 | * 'path' => 'folder-name' 142 | * 'path' => 'folder1*' - select folder1, folder12, folder1/sub-folder, ... 143 | * 'path' => 'folder2/*' - select folder2/sub-folder,... but not select folder2 !!! 144 | * 'path' => 'folder-name/file-name.jpg' 145 | * 'path' => 'folder-name/*.jpg' 146 | * 147 | * * - wildcard 148 | * 149 | * access: 0 - deny, 1 - read, 2 - read/write 150 | */ 151 | 'aclRules' => [ 152 | null => [ 153 | //['disk' => 'public', 'path' => '/', 'access' => 2], 154 | ], 155 | 1 => [ 156 | //['disk' => 'public', 'path' => 'images/arch*.jpg', 'access' => 2], 157 | //['disk' => 'public', 'path' => 'files/*', 'access' => 1], 158 | ], 159 | ], 160 | 161 | /** 162 | * Enable slugification of filenames of uploaded files. 163 | * 164 | */ 165 | 'slugifyNames' => false, 166 | ]; 167 | -------------------------------------------------------------------------------- /src/Services/ConfigService/DefaultConfigRepository.php: -------------------------------------------------------------------------------- 1 | ['web', 'auth', 'admin'] 131 | * !!!! RESTRICT ACCESS FOR NON ADMIN USERS !!!! 132 | * 133 | * @return array 134 | */ 135 | final public function getMiddleware(): array 136 | { 137 | return config('file-manager.middleware'); 138 | } 139 | 140 | /** 141 | * ACL mechanism ON/OFF 142 | * 143 | * default - false(OFF) 144 | * 145 | * @return bool 146 | */ 147 | final public function getAcl(): bool 148 | { 149 | return config('file-manager.acl'); 150 | } 151 | 152 | /** 153 | * Hide files and folders from file-manager if user doesn't have access 154 | * 155 | * ACL access level = 0 156 | * 157 | * @return bool 158 | */ 159 | final public function getAclHideFromFM(): bool 160 | { 161 | return config('file-manager.aclHideFromFM'); 162 | } 163 | 164 | /** 165 | * ACL strategy 166 | * 167 | * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list 168 | * 169 | * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list 170 | * 171 | * @return string 172 | */ 173 | final public function getAclStrategy(): string 174 | { 175 | return config('file-manager.aclStrategy'); 176 | } 177 | 178 | /** 179 | * ACL rules repository 180 | * 181 | * default - config file(ConfigACLRepository) 182 | * 183 | * @return string 184 | */ 185 | final public function getAclRepository(): string 186 | { 187 | return config('file-manager.aclRepository'); 188 | } 189 | 190 | /** 191 | * ACL Rules cache 192 | * 193 | * null or value in minutes 194 | * 195 | * @return int|null 196 | */ 197 | final public function getAclRulesCache(): ?int 198 | { 199 | return config('file-manager.aclRulesCache'); 200 | } 201 | 202 | /** 203 | * Whether to slugify filenames 204 | * 205 | * boolean 206 | * 207 | * @return bool|null 208 | */ 209 | final public function getSlugifyNames(): ?bool 210 | { 211 | return config('file-manager.slugifyNames', false); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /docs/acl.md: -------------------------------------------------------------------------------- 1 | # ACL 2 | 3 | You can use the access control system to differentiate access to files and folders for different users. 4 | For this you need to make the following settings. 5 | Open configuration file - config/file-manager.php 6 | 7 | 1. Turn ON ACL system (fm-acl middleware will turn ON automatically) 8 | 9 | ```php 10 | // set true 11 | 'acl' => true, 12 | ``` 13 | 14 | 2. You can hide files and folders to which the user does not have access(access = 0). 15 | 16 | ```php 17 | 'aclHideFromFM' => true, 18 | ``` 19 | 3. ACL system operation strategies: 20 | 21 | ```php 22 | /** 23 | * ACL strategy 24 | * 25 | * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list 26 | * 27 | * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list 28 | */ 29 | 'aclStrategy' => 'blacklist', 30 | ``` 31 | 32 | 4. Set the rule repository, the default is the configuration file. 33 | 34 | ```php 35 | /** 36 | * ACL rules repository 37 | * 38 | * default - config file(ConfigACLRepository) 39 | */ 40 | 'aclRepository' => \Alexusmai\LaravelFileManager\Services\ACLService\ConfigACLRepository::class, 41 | ``` 42 | 43 | Now you can add your rules in 'aclRules' array. But if you want to store your rules in another place, such as a database, you need to create your own class, and implements two functions from ACLRepository. 44 | 45 | I have already made a similar class for an example, and if it suits you, you can use it. You only need to replace the repository name in the configuration file. And add a new migration to the database. 46 | 47 | ```php 48 | php artisan vendor:publish --tag=fm-migrations 49 | ``` 50 | 51 | See [/src/Services/ACLService/DBACLRepository.php](../src/Services/ACLService/DBACLRepository.php) and [/migrations/2019_02_06_174631_make_acl_rules_table.php](./../migrations/2019_02_06_174631_make_acl_rules_table.php) 52 | 53 | ## Example 1 54 | 55 | I have disk 'images' in /config/filesystems.php for folder /public/images 56 | 57 | ```php 58 | 'disks' => [ 59 | 60 | 'images' => [ 61 | 'driver' => 'local', 62 | 'root' => public_path('images'), 63 | 'url' => env('APP_URL').'/images/', 64 | ], 65 | ] 66 | ``` 67 | 68 | This disk contain: 69 | 70 | ```php 71 | / // disk root folder 72 | |-- nature // folder 73 | |-- cars // folder 74 | |-- icons 75 | |-- image1.jpg // file 76 | |-- image2.jpg 77 | |-- avatar.png 78 | ``` 79 | 80 | I add this disk to file-manager config file 81 | 82 | ```php 83 | 'diskList' => ['images'], 84 | 85 | 'aclStrategy' => 'blacklist', 86 | 87 | // now it's a black list 88 | 'aclRules' => [ 89 | // null - for not authenticated users 90 | null => [ 91 | ['disk' => 'images', 'path' => 'nature', 'access' => 0], // guest don't have access for this folder 92 | ['disk' => 'images', 'path' => 'icons', 'access' => 1], // only read - guest can't change folder - rename, delete 93 | ['disk' => 'images', 'path' => 'icons/*', 'access' => 1], // only read all files and foders in this folder 94 | ['disk' => 'images', 'path' => 'image*.jpg', 'access' => 0], // can't read and write (preview, rename, delete..) 95 | ['disk' => 'images', 'path' => 'avatar.png', 'access' => 1], // only read (view) 96 | 97 | ], 98 | // for user with ID = 1 99 | 1 => [ 100 | ['disk' => 'images', 'path' => 'cars', 'access' => 0], // don't have access 101 | ['disk' => 'public', 'path' => 'image*.jpg', 'access' => 1], // only read (view) 102 | ], 103 | ], 104 | ``` 105 | 106 | ## Example 2 107 | 108 | > Task: For each registered user, a new folder is created with his name(in folder /users). You want to allow users access only to their folders. But for an administrator with ID = 1, allow access to all folders. 109 | 110 | - You need to create a new repository for ACL rules, for example, in the / app / Http folder 111 | 112 | ```php 113 | 'disk-name', 'path' => '*', 'access' => 2], 141 | ]; 142 | } 143 | 144 | return [ 145 | ['disk' => 'disk-name', 'path' => '/', 'access' => 1], // main folder - read 146 | ['disk' => 'disk-name', 'path' => 'users', 'access' => 1], // only read 147 | ['disk' => 'disk-name', 'path' => 'users/'. \Auth::user()->name, 'access' => 1], // only read 148 | ['disk' => 'disk-name', 'path' => 'users/'. \Auth::user()->name .'/*', 'access' => 2], // read and write 149 | ]; 150 | } 151 | } 152 | ``` 153 | 154 | - disk-name - you need to replace for your disk name 155 | 156 | - now in the config file we will change the repository to a new one, and set aclStrategy in whitelist - we will deny everything that is not allowed by the rules. You can also hide folders and files that are not available. 157 | 158 | ```php 159 | /** 160 | * Hide files and folders from file-manager if user doesn't have access 161 | * ACL access level = 0 162 | */ 163 | 'aclHideFromFM' => true, 164 | 165 | /** 166 | * ACL strategy 167 | * 168 | * blacklist - Allow everything(access - 2 - r/w) that is not forbidden by the ACL rules list 169 | * 170 | * whitelist - Deny anything(access - 0 - deny), that not allowed by the ACL rules list 171 | */ 172 | 'aclStrategy' => 'whitelist', 173 | 174 | /** 175 | * ACL rules repository 176 | * 177 | * default - config file(ConfigACLRepository) 178 | */ 179 | 'aclRepository' => \App\Http\UsersACLRepository::class, 180 | ``` 181 | 182 | 183 | ## What's next 184 | 185 | [Events](./events.md) 186 | -------------------------------------------------------------------------------- /docs/integration.md: -------------------------------------------------------------------------------- 1 | # Integration 2 | 3 | > See examples in [examples](./../examples) folder 4 | 5 | ### CKEditor 4 6 | 7 | Add to CKEditor config 8 | 9 | ```js 10 | CKEDITOR.replace('editor-id', {filebrowserImageBrowseUrl: '/file-manager/ckeditor'}); 11 | ``` 12 | 13 | OR in to the config.js 14 | 15 | ```js 16 | CKEDITOR.editorConfig = function( config ) { 17 | 18 | //... 19 | 20 | // Upload image 21 | config.filebrowserImageBrowseUrl = '/file-manager/ckeditor'; 22 | }; 23 | ``` 24 | 25 | After these actions, you will be able to call the file manager from the CKEditor editor menu (Image -> Selection on the server). 26 | The file manager will appear in a new window. 27 | 28 | ### TinyMCE 4 29 | 30 | Add to TinyMCE configuration 31 | 32 | ```js 33 | tinymce.init({ 34 | selector: '#my-textarea', 35 | // ... 36 | file_browser_callback: function(field_name, url, type, win) { 37 | tinyMCE.activeEditor.windowManager.open({ 38 | file: '/file-manager/tinymce', 39 | title: 'Laravel File Manager', 40 | width: window.innerWidth * 0.8, 41 | height: window.innerHeight * 0.8, 42 | resizable: 'yes', 43 | close_previous: 'no', 44 | }, { 45 | setUrl: function(url) { 46 | win.document.getElementById(field_name).value = url; 47 | }, 48 | }); 49 | }, 50 | }); 51 | ``` 52 | 53 | ### TinyMCE 5 54 | 55 | Add to TinyMCE 5 configuration 56 | 57 | ```js 58 | tinymce.init({ 59 | selector: '#my-textarea', 60 | // ... 61 | file_picker_callback (callback, value, meta) { 62 | let x = window.innerWidth || document.documentElement.clientWidth || document.getElementsByTagName('body')[0].clientWidth 63 | let y = window.innerHeight|| document.documentElement.clientHeight|| document.getElementsByTagName('body')[0].clientHeight 64 | 65 | tinymce.activeEditor.windowManager.openUrl({ 66 | url : '/file-manager/tinymce5', 67 | title : 'Laravel File manager', 68 | width : x * 0.8, 69 | height : y * 0.8, 70 | onMessage: (api, message) => { 71 | callback(message.content, { text: message.text }) 72 | } 73 | }) 74 | } 75 | }); 76 | ``` 77 | 78 | ### SummerNote 79 | 80 | Create and add new button 81 | 82 | ```js 83 | // File manager button (image icon) 84 | const FMButton = function(context) { 85 | const ui = $.summernote.ui; 86 | const button = ui.button({ 87 | contents: ' ', 88 | tooltip: 'File Manager', 89 | click: function() { 90 | window.open('/file-manager/summernote', 'fm', 'width=1400,height=800'); 91 | } 92 | }); 93 | return button.render(); 94 | }; 95 | 96 | $('#summernote').summernote({ 97 | toolbar: [ 98 | // [groupName, [list of button]] 99 | // your settings 100 | ['fm-button', ['fm']], 101 | ], 102 | buttons: { 103 | fm: FMButton 104 | } 105 | }); 106 | ``` 107 | 108 | And add this function 109 | 110 | ```js 111 | // set file link 112 | function fmSetLink(url) { 113 | $('#summernote').summernote('insertImage', url); 114 | } 115 | ``` 116 | 117 | See [example](./../examples/wysiwyg/summernote.blade.php) 118 | 119 | ### Standalone button 120 | 121 | Add button 122 | 123 | ```html 124 |
125 | 127 |
128 | 129 |
130 |
131 | ``` 132 | 133 | and js script 134 | 135 | ```js 136 | document.addEventListener("DOMContentLoaded", function() { 137 | 138 | document.getElementById('button-image').addEventListener('click', (event) => { 139 | event.preventDefault(); 140 | 141 | window.open('/file-manager/fm-button', 'fm', 'width=1400,height=800'); 142 | }); 143 | }); 144 | 145 | // set file link 146 | function fmSetLink($url) { 147 | document.getElementById('image_label').value = $url; 148 | } 149 | ``` 150 | 151 | ### Multiple standalone buttons 152 | 153 | ```html 154 | 155 |
156 |
157 |
158 | 159 |
160 | 162 |
163 | 164 |
165 |
166 |
167 |
168 | 169 |
170 | 172 |
173 | 174 |
175 |
176 |
177 |
178 |
179 | 180 | 181 | 210 | ``` 211 | 212 | ### Modifications 213 | 214 | To change standard views(with file manager), publish them. 215 | 216 | ```bash 217 | php artisan vendor:publish --tag=fm-views 218 | ``` 219 | 220 | You will get: 221 | 222 | ``` 223 | resources/views/vendor/file-manager/ckeditor.blade.php 224 | resources/views/vendor/file-manager/tinymce.blade.php 225 | resources/views/vendor/file-manager/summernote.blade.php 226 | resources/views/vendor/file-manager/fmButton.blade.php 227 | ``` 228 | 229 | Now you can change styles and any more.. 230 | -------------------------------------------------------------------------------- /src/Services/Zip.php: -------------------------------------------------------------------------------- 1 | zip = $zip; 30 | $this->request = $request; 31 | //$this->pathPrefix = Storage::disk($request->input('disk'))->path(); 32 | //->getDriver() 33 | //->getAdapter() 34 | //->getPathPrefix(); 35 | } 36 | 37 | /** 38 | * Create new zip archive 39 | * 40 | * @return array 41 | */ 42 | public function create(): array 43 | { 44 | if ($this->createArchive()) { 45 | return [ 46 | 'result' => [ 47 | 'status' => 'success', 48 | 'message' => null, 49 | ], 50 | ]; 51 | } 52 | 53 | return [ 54 | 'result' => [ 55 | 'status' => 'warning', 56 | 'message' => 'zipError', 57 | ], 58 | ]; 59 | } 60 | 61 | /** 62 | * Extract 63 | * 64 | * @return array 65 | */ 66 | public function extract(): array 67 | { 68 | if ($this->extractArchive()) { 69 | return [ 70 | 'result' => [ 71 | 'status' => 'success', 72 | 'message' => null, 73 | ], 74 | ]; 75 | } 76 | 77 | return [ 78 | 'result' => [ 79 | 'status' => 'warning', 80 | 'message' => 'zipError', 81 | ], 82 | ]; 83 | } 84 | 85 | protected function prefixer($path): string 86 | { 87 | return Storage::disk($this->request->input('disk'))->path($path); 88 | } 89 | 90 | /** 91 | * Create zip archive 92 | * 93 | * @return bool 94 | */ 95 | protected function createArchive(): bool 96 | { 97 | // elements list 98 | $elements = $this->request->input('elements'); 99 | 100 | // Check files for traversal 101 | if (isset($elements['files']) && is_array($elements['files'])) { 102 | foreach ($elements['files'] as $file) { 103 | if (strpos($file, '..') !== false) { 104 | event(new ZipFailed($this->request)); 105 | return false; 106 | } 107 | } 108 | } 109 | 110 | // Check directories for traversal 111 | if (isset($elements['directories']) && is_array($elements['directories'])) { 112 | foreach ($elements['directories'] as $directory) { 113 | if (strpos($directory, '..') !== false) { 114 | event(new ZipFailed($this->request)); 115 | return false; 116 | } 117 | } 118 | } 119 | 120 | // create or overwrite archive 121 | if ($this->zip->open( 122 | $this->createName(), 123 | ZIPARCHIVE::OVERWRITE | ZIPARCHIVE::CREATE 124 | ) === true 125 | ) { 126 | if (isset($elements['files']) && $elements['files']) { 127 | foreach ($elements['files'] as $file) { 128 | $this->zip->addFile( 129 | $this->prefixer($file), 130 | basename($file) 131 | ); 132 | } 133 | } 134 | 135 | if (isset($elements['directories']) && $elements['directories']) { 136 | $this->addDirs($elements['directories']); 137 | } 138 | 139 | $this->zip->close(); 140 | 141 | event(new ZipCreated($this->request)); 142 | 143 | return true; 144 | } 145 | 146 | event(new ZipFailed($this->request)); 147 | 148 | return false; 149 | } 150 | 151 | /** 152 | * Archive extract 153 | * 154 | * @return bool 155 | */ 156 | protected function extractArchive(): bool 157 | { 158 | $zipPath = $this->prefixer($this->request->input('path')); 159 | $rootPath = dirname($zipPath); 160 | 161 | // extract to new folder 162 | $folder = $this->request->input('folder'); 163 | 164 | if ($folder && (strpos($folder, '..') !== false || strpos($folder, '://') !== false)) { 165 | event(new UnzipFailed($this->request)); 166 | return false; 167 | } 168 | 169 | if ($this->zip->open($zipPath) === true) { 170 | $this->zip->extractTo($folder ? $rootPath.'/'.$folder : $rootPath); 171 | $this->zip->close(); 172 | 173 | event(new UnzipCreated($this->request)); 174 | 175 | return true; 176 | } 177 | 178 | event(new UnzipFailed($this->request)); 179 | 180 | return false; 181 | } 182 | 183 | /** 184 | * Add directories - recursive 185 | * 186 | * @param array $directories 187 | */ 188 | protected function addDirs(array $directories) 189 | { 190 | foreach ($directories as $directory) { 191 | 192 | // Create recursive directory iterator 193 | $files = new RecursiveIteratorIterator( 194 | new RecursiveDirectoryIterator($this->prefixer($directory)), 195 | RecursiveIteratorIterator::LEAVES_ONLY 196 | ); 197 | 198 | foreach ($files as $name => $file) { 199 | // Get real and relative path for current item 200 | $filePath = $file->getRealPath(); 201 | $relativePath = substr( 202 | $filePath, 203 | strlen($this->fullPath($this->request->input('path'))) 204 | ); 205 | 206 | if (!$file->isDir()) { 207 | // Add current file to archive 208 | $this->zip->addFile($filePath, $relativePath); 209 | } else { 210 | // add empty folders 211 | if (!glob($filePath.'/*')) { 212 | $this->zip->addEmptyDir($relativePath); 213 | } 214 | } 215 | } 216 | } 217 | } 218 | 219 | /** 220 | * Create archive name with full path 221 | * 222 | * @return string 223 | */ 224 | protected function createName(): string 225 | { 226 | return $this->fullPath($this->request->input('path')) 227 | .$this->request->input('name'); 228 | } 229 | 230 | /** 231 | * Generate full path 232 | * 233 | * @param $path 234 | * 235 | * @return string 236 | */ 237 | protected function fullPath($path): string 238 | { 239 | return $path ? $this->prefixer($path).'/' : $this->prefixer(''); 240 | } 241 | } -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # Events 2 | 3 | ### BeforeInitialization 4 | 5 | > Alexusmai\LaravelFileManager\Events\BeforeInitialization 6 | 7 | Example: 8 | 9 | ```php 10 | \Event::listen('Alexusmai\LaravelFileManager\Events\BeforeInitialization', 11 | function ($event) { 12 | 13 | } 14 | ); 15 | ``` 16 | 17 | ### DiskSelected 18 | 19 | > Alexusmai\LaravelFileManager\Events\DiskSelected 20 | 21 | Example: 22 | 23 | ```php 24 | \Event::listen('Alexusmai\LaravelFileManager\Events\DiskSelected', 25 | function ($event) { 26 | \Log::info('DiskSelected:', [$event->disk()]); 27 | } 28 | ); 29 | ``` 30 | 31 | ### FilesUploading 32 | 33 | > Alexusmai\LaravelFileManager\Events\FilesUploading 34 | 35 | ```php 36 | \Event::listen('Alexusmai\LaravelFileManager\Events\FilesUploading', 37 | function ($event) { 38 | \Log::info('FilesUploading:', [ 39 | $event->disk(), 40 | $event->path(), 41 | $event->files(), 42 | $event->overwrite(), 43 | ]); 44 | } 45 | ); 46 | ``` 47 | 48 | ### FilesUploaded 49 | 50 | > Alexusmai\LaravelFileManager\Events\FilesUploaded 51 | 52 | ```php 53 | \Event::listen('Alexusmai\LaravelFileManager\Events\FilesUploaded', 54 | function ($event) { 55 | \Log::info('FilesUploaded:', [ 56 | $event->disk(), 57 | $event->path(), 58 | $event->files(), 59 | $event->overwrite(), 60 | ]); 61 | } 62 | ); 63 | ``` 64 | 65 | ### Deleting 66 | 67 | > Alexusmai\LaravelFileManager\Events\Deleting 68 | 69 | ```php 70 | \Event::listen('Alexusmai\LaravelFileManager\Events\Deleting', 71 | function ($event) { 72 | \Log::info('Deleting:', [ 73 | $event->disk(), 74 | $event->items(), 75 | ]); 76 | } 77 | ); 78 | ``` 79 | 80 | ### Deleted 81 | 82 | > Alexusmai\LaravelFileManager\Events\Deleted 83 | 84 | ```php 85 | \Event::listen('Alexusmai\LaravelFileManager\Events\Deleted', 86 | function ($event) { 87 | \Log::info('Deleted:', [ 88 | $event->disk(), 89 | $event->items(), 90 | ]); 91 | } 92 | ); 93 | ``` 94 | 95 | ### Paste 96 | 97 | > Alexusmai\LaravelFileManager\Events\Paste 98 | 99 | ```php 100 | \Event::listen('Alexusmai\LaravelFileManager\Events\Paste', 101 | function ($event) { 102 | \Log::info('Paste:', [ 103 | $event->disk(), 104 | $event->path(), 105 | $event->clipboard(), 106 | ]); 107 | } 108 | ); 109 | ``` 110 | 111 | ### Rename 112 | 113 | > Alexusmai\LaravelFileManager\Events\Rename 114 | 115 | ```php 116 | \Event::listen('Alexusmai\LaravelFileManager\Events\Rename', 117 | function ($event) { 118 | \Log::info('Rename:', [ 119 | $event->disk(), 120 | $event->newName(), 121 | $event->oldName(), 122 | $event->type(), // 'file' or 'dir' 123 | ]); 124 | } 125 | ); 126 | ``` 127 | 128 | ### Download 129 | 130 | > Alexusmai\LaravelFileManager\Events\Download 131 | 132 | ```php 133 | \Event::listen('Alexusmai\LaravelFileManager\Events\Download', 134 | function ($event) { 135 | \Log::info('Download:', [ 136 | $event->disk(), 137 | $event->path(), 138 | ]); 139 | } 140 | ); 141 | ``` 142 | 143 | *When using a text editor, the file you are editing is also downloaded! And this event is triggered!* 144 | 145 | ### DirectoryCreating 146 | 147 | > Alexusmai\LaravelFileManager\Events\DirectoryCreating 148 | 149 | ```php 150 | \Event::listen('Alexusmai\LaravelFileManager\Events\DirectoryCreating', 151 | function ($event) { 152 | \Log::info('DirectoryCreating:', [ 153 | $event->disk(), 154 | $event->path(), 155 | $event->name(), 156 | ]); 157 | } 158 | ); 159 | ``` 160 | 161 | ### DirectoryCreated 162 | 163 | > Alexusmai\LaravelFileManager\Events\DirectoryCreated 164 | 165 | ```php 166 | \Event::listen('Alexusmai\LaravelFileManager\Events\DirectoryCreated', 167 | function ($event) { 168 | \Log::info('DirectoryCreated:', [ 169 | $event->disk(), 170 | $event->path(), 171 | $event->name(), 172 | ]); 173 | } 174 | ); 175 | ``` 176 | 177 | ### FileCreating 178 | 179 | > Alexusmai\LaravelFileManager\Events\FileCreating 180 | 181 | ```php 182 | \Event::listen('Alexusmai\LaravelFileManager\Events\FileCreating', 183 | function ($event) { 184 | \Log::info('FileCreating:', [ 185 | $event->disk(), 186 | $event->path(), 187 | $event->name(), 188 | ]); 189 | } 190 | ); 191 | ``` 192 | 193 | ### FileCreated 194 | 195 | > Alexusmai\LaravelFileManager\Events\FileCreated 196 | 197 | ```php 198 | \Event::listen('Alexusmai\LaravelFileManager\Events\FileCreated', 199 | function ($event) { 200 | \Log::info('FileCreated:', [ 201 | $event->disk(), 202 | $event->path(), 203 | $event->name(), 204 | ]); 205 | } 206 | ); 207 | ``` 208 | 209 | ### FileUpdate 210 | 211 | > Alexusmai\LaravelFileManager\Events\FileUpdate 212 | 213 | ```php 214 | \Event::listen('Alexusmai\LaravelFileManager\Events\FileUpdate', 215 | function ($event) { 216 | \Log::info('FileUpdate:', [ 217 | $event->disk(), 218 | $event->path(), 219 | ]); 220 | } 221 | ); 222 | ``` 223 | 224 | ### Zip 225 | 226 | > Alexusmai\LaravelFileManager\Events\Zip 227 | 228 | ```php 229 | \Event::listen('Alexusmai\LaravelFileManager\Events\Zip', 230 | function ($event) { 231 | \Log::info('Zip:', [ 232 | $event->disk(), 233 | $event->path(), 234 | $event->name(), 235 | $event->elements(), 236 | ]); 237 | } 238 | ); 239 | ``` 240 | 241 | ### ZipCreated 242 | 243 | > Alexusmai\LaravelFileManager\Events\ZipCreated 244 | 245 | ```php 246 | \Event::listen('Alexusmai\LaravelFileManager\Events\ZipCreated', 247 | function ($event) { 248 | \Log::info('ZipCreated:', [ 249 | $event->disk(), 250 | $event->path(), 251 | $event->name(), 252 | $event->elements(), 253 | ]); 254 | } 255 | ); 256 | ``` 257 | 258 | ### ZipFailed 259 | 260 | > Alexusmai\LaravelFileManager\Events\ZipCreated 261 | 262 | ```php 263 | \Event::listen('Alexusmai\LaravelFileManager\Events\ZipFailed', 264 | function ($event) { 265 | \Log::info('ZipFailed:', [ 266 | $event->disk(), 267 | $event->path(), 268 | $event->name(), 269 | $event->elements(), 270 | ]); 271 | } 272 | ); 273 | ``` 274 | 275 | ### Unzip 276 | 277 | > Alexusmai\LaravelFileManager\Events\Unzip 278 | 279 | ```php 280 | \Event::listen('Alexusmai\LaravelFileManager\Events\Unzip', 281 | function ($event) { 282 | \Log::info('Unzip:', [ 283 | $event->disk(), 284 | $event->path(), 285 | $event->folder(), 286 | ]); 287 | } 288 | ); 289 | ``` 290 | 291 | ### UnzipCreated 292 | 293 | > Alexusmai\LaravelFileManager\Events\UnzipCreated 294 | 295 | ```php 296 | \Event::listen('Alexusmai\LaravelFileManager\Events\UnzipCreated', 297 | function ($event) { 298 | \Log::info('UnzipCreated:', [ 299 | $event->disk(), 300 | $event->path(), 301 | $event->folder(), 302 | ]); 303 | } 304 | ); 305 | ``` 306 | 307 | ### UnzipFailed 308 | 309 | > Alexusmai\LaravelFileManager\Events\UnzipFailed 310 | 311 | ```php 312 | \Event::listen('Alexusmai\LaravelFileManager\Events\UnzipFailed', 313 | function ($event) { 314 | \Log::info('UnzipFailed:', [ 315 | $event->disk(), 316 | $event->path(), 317 | $event->folder(), 318 | ]); 319 | } 320 | ); 321 | ``` 322 | -------------------------------------------------------------------------------- /src/Traits/ContentTrait.php: -------------------------------------------------------------------------------- 1 | listContents($path ?: '')->toArray(); 27 | 28 | $directories = $this->filterDir($disk, $content); 29 | $files = $this->filterFile($disk, $content); 30 | 31 | return compact('directories', 'files'); 32 | } 33 | 34 | /** 35 | * Get directories with properties 36 | * 37 | * @param $disk 38 | * @param null $path 39 | * 40 | * @return array 41 | * @throws FilesystemException 42 | */ 43 | public function directoriesWithProperties($disk, $path = null): array 44 | { 45 | $content = Storage::disk($disk)->listContents($path ?: '')->toArray(); 46 | 47 | return $this->filterDir($disk, $content); 48 | } 49 | 50 | /** 51 | * Get files with properties 52 | * 53 | * @param $disk 54 | * @param null $path 55 | * 56 | * @return array 57 | * @throws FilesystemException 58 | */ 59 | public function filesWithProperties($disk, $path = null): array 60 | { 61 | $content = Storage::disk($disk)->listContents($path ?: ''); 62 | 63 | return $this->filterFile($disk, $content); 64 | } 65 | 66 | /** 67 | * Get directories for tree module 68 | * 69 | * @param $disk 70 | * @param null $path 71 | * 72 | * @return array 73 | * @throws FilesystemException 74 | */ 75 | public function getDirectoriesTree($disk, $path = null): array 76 | { 77 | $directories = $this->directoriesWithProperties($disk, $path); 78 | 79 | foreach ($directories as $index => $dir) { 80 | $directories[$index]['props'] = [ 81 | 'hasSubdirectories' => (bool) Storage::disk($disk)->directories($dir['path']), 82 | ]; 83 | } 84 | 85 | return $directories; 86 | } 87 | 88 | /** 89 | * File properties 90 | * 91 | * @param $disk 92 | * @param $path 93 | * 94 | * @return mixed 95 | */ 96 | public function fileProperties($disk, $path = null): mixed 97 | { 98 | $pathInfo = pathinfo($path); 99 | 100 | $properties = [ 101 | 'type' => 'file', 102 | 'path' => $path, 103 | 'basename' => $pathInfo['basename'], 104 | 'dirname' => $pathInfo['dirname'] === '.' ? '' : $pathInfo['dirname'], 105 | 'extension' => $pathInfo['extension'] ?? '', 106 | 'filename' => $pathInfo['filename'], 107 | 'size' => Storage::disk($disk)->size($path), 108 | 'timestamp' => Storage::disk($disk)->lastModified($path), 109 | 'visibility' => Storage::disk($disk)->getVisibility($path), 110 | ]; 111 | 112 | // if ACL ON 113 | if ($this->configRepository->getAcl()) { 114 | return $this->aclFilter($disk, [$properties])[0]; 115 | } 116 | 117 | return $properties; 118 | } 119 | 120 | /** 121 | * Get properties for the selected directory 122 | * 123 | * @param $disk 124 | * @param null $path 125 | * 126 | * @return array|false 127 | */ 128 | public function directoryProperties($disk, $path = null): bool|array 129 | { 130 | $adapter = Storage::drive($disk)->getAdapter(); 131 | 132 | $pathInfo = pathinfo($path); 133 | 134 | if ( 135 | $adapter instanceof AwsS3V3Adapter || 136 | $adapter instanceof FtpAdapter || 137 | $adapter instanceof SftpAdapter 138 | ){ 139 | $timestamp = null; 140 | $visibility = null; 141 | } else { 142 | $timestamp = Storage::disk($disk)->lastModified($path); 143 | $visibility = Storage::disk($disk)->getVisibility($path); 144 | } 145 | 146 | $properties = [ 147 | 'type' => 'dir', 148 | 'path' => $path, 149 | 'basename' => $pathInfo['basename'], 150 | 'dirname' => $pathInfo['dirname'] === '.' ? '' : $pathInfo['dirname'], 151 | 'timestamp' => $timestamp, 152 | 'visibility' => $visibility, 153 | ]; 154 | 155 | // if ACL ON 156 | if ($this->configRepository->getAcl()) { 157 | return $this->aclFilter($disk, [$properties])[0]; 158 | } 159 | 160 | return $properties; 161 | } 162 | 163 | /** 164 | * Get only directories 165 | * 166 | * @param $disk 167 | * @param $content 168 | * 169 | * @return array 170 | */ 171 | protected function filterDir($disk, $content): array 172 | { 173 | // select only dir 174 | $dirsList = array_filter($content, fn($item) => $item['type'] === 'dir'); 175 | 176 | $dirs = array_map(function ($item) { 177 | $pathInfo = pathinfo($item['path']); 178 | 179 | return [ 180 | 'type' => $item['type'], 181 | 'path' => $item['path'], 182 | 'basename' => $pathInfo['basename'], 183 | 'dirname' => $pathInfo['dirname'] === '.' ? '' : $pathInfo['dirname'], 184 | 'timestamp' => $item['lastModified'], 185 | 'visibility' => $item['visibility'], 186 | ]; 187 | }, $dirsList); 188 | 189 | // if ACL ON 190 | if ($this->configRepository->getAcl()) { 191 | return array_values($this->aclFilter($disk, $dirs)); 192 | } 193 | 194 | return array_values($dirs); 195 | } 196 | 197 | /** 198 | * Get only files 199 | * 200 | * @param $disk 201 | * @param $content 202 | * 203 | * @return array 204 | */ 205 | protected function filterFile($disk, $content): array 206 | { 207 | // select only dir 208 | $filesList = array_filter($content, fn($item) => $item['type'] === 'file'); 209 | 210 | $files = array_map(function ($item) { 211 | $pathInfo = pathinfo($item['path']); 212 | 213 | return [ 214 | 'type' => $item['type'], 215 | 'path' => $item['path'], 216 | 'basename' => $pathInfo['basename'], 217 | 'dirname' => $pathInfo['dirname'] === '.' ? '' : $pathInfo['dirname'], 218 | 'extension' => $pathInfo['extension'] ?? '', 219 | 'filename' => $pathInfo['filename'], 220 | 'size' => $item['fileSize'], 221 | 'timestamp' => $item['lastModified'], 222 | 'visibility' => $item['visibility'], 223 | ]; 224 | }, $filesList); 225 | 226 | // if ACL ON 227 | if ($this->configRepository->getAcl()) { 228 | return array_values($this->aclFilter($disk, $files)); 229 | } 230 | 231 | return array_values($files); 232 | } 233 | 234 | /** 235 | * ACL filter 236 | * 237 | * @param $disk 238 | * @param $content 239 | * 240 | * @return mixed 241 | */ 242 | protected function aclFilter($disk, $content): mixed 243 | { 244 | $acl = resolve(ACL::class); 245 | 246 | $withAccess = array_map(function ($item) use ($acl, $disk) { 247 | // add acl access level 248 | $item['acl'] = $acl->getAccessLevel($disk, $item['path']); 249 | 250 | return $item; 251 | }, $content); 252 | 253 | // filter files and folders 254 | if ($this->configRepository->getAclHideFromFM()) { 255 | return array_filter($withAccess, function ($item) { 256 | return $item['acl'] !== 0; 257 | }); 258 | } 259 | 260 | return $withAccess; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/Middleware/FileManagerACL.php: -------------------------------------------------------------------------------- 1 | 'checkContent', 21 | 'fm.content' => 'checkContent', 22 | 'fm.preview' => 'checkContent', 23 | 'fm.thumbnails' => 'checkContent', 24 | 'fm.url' => 'checkContent', 25 | 'fm.stream-file' => 'checkContent', 26 | 'fm.download' => 'checkDownload', 27 | 'fm.create-file' => 'checkCreate', 28 | 'fm.create-directory' => 'checkCreate', 29 | 'fm.update-file' => 'checkUpdate', 30 | 'fm.upload' => 'checkUpload', 31 | 'fm.delete' => 'checkDelete', 32 | 'fm.paste' => 'checkPaste', 33 | 'fm.rename' => 'checkRename', 34 | 'fm.zip' => 'checkZip', 35 | 'fm.unzip' => 'checkUnzip', 36 | ]; 37 | 38 | /** 39 | * @var string|null 40 | */ 41 | protected $disk; 42 | 43 | /** 44 | * @var string|null 45 | */ 46 | protected $path; 47 | 48 | /** 49 | * @var ACL|mixed 50 | */ 51 | protected $acl; 52 | 53 | /** 54 | * @var Request 55 | */ 56 | protected $request; 57 | 58 | /** 59 | * FileManagerACL constructor. 60 | * 61 | * @param Request $request 62 | * @param ACL $acl 63 | */ 64 | public function __construct(Request $request, ACL $acl) 65 | { 66 | $this->disk = $request->has('disk') ? $request->input('disk') : null; 67 | $this->path = $request->has('path') ? $request->input('path') : '/'; 68 | 69 | $this->acl = $acl; 70 | 71 | $this->request = $request; 72 | } 73 | 74 | /** 75 | * Handle an incoming request. 76 | * 77 | * @param \Illuminate\Http\Request $request 78 | * @param \Closure $next 79 | * 80 | * @return mixed 81 | */ 82 | public function handle($request, Closure $next) 83 | { 84 | $routeName = $request->route()->getName(); 85 | 86 | // if ACL is OFF or route name wasn't found 87 | if ( ! resolve(ConfigRepository::class)->getAcl() 88 | || ! array_key_exists($routeName, self::CHECKERS) 89 | ) { 90 | return $next($request); 91 | } 92 | 93 | if ( ! call_user_func([$this, self::CHECKERS[$routeName]])) { 94 | return $this->errorMessage(); 95 | } 96 | 97 | // return request 98 | return $next($request); 99 | } 100 | 101 | /** 102 | * ACL Error message 103 | * 104 | * @return \Illuminate\Http\JsonResponse 105 | */ 106 | protected function errorMessage() 107 | { 108 | return response()->json([ 109 | 'result' => [ 110 | 'status' => 'error', 111 | 'message' => 'aclError', 112 | ], 113 | ]); 114 | } 115 | 116 | /** 117 | * Check content actions 118 | * 119 | * @return bool 120 | */ 121 | protected function checkContent() 122 | { 123 | // need r access 124 | return ! ($this->acl->getAccessLevel($this->disk, $this->path) === 0); 125 | } 126 | 127 | /** 128 | * Check download actions 129 | */ 130 | protected function checkDownload() 131 | { 132 | // need r access 133 | abort_if( 134 | $this->acl->getAccessLevel($this->disk, $this->path) === 0, 135 | 403 136 | ); 137 | 138 | return true; 139 | } 140 | 141 | /** 142 | * Check create actions 143 | * 144 | * @return bool 145 | */ 146 | protected function checkCreate() 147 | { 148 | $name = $this->request->input('name'); 149 | $pathToWrite = $this->request->input('path') 150 | ? $this->request->input('path').'/' : ''; 151 | 152 | // need r/w access 153 | return ! ($this->acl->getAccessLevel($this->disk, $pathToWrite.$name) !== 2); 154 | } 155 | 156 | /** 157 | * Check update actions 158 | * 159 | * @return bool 160 | */ 161 | protected function checkUpdate() 162 | { 163 | $pathToWrite = $this->request->input('path') 164 | ? $this->request->input('path').'/' : ''; 165 | 166 | $name = $this->request->file('file')->getClientOriginalName(); 167 | 168 | // need r/w access 169 | return ! ($this->acl->getAccessLevel($this->disk, $pathToWrite.$name) !== 2); 170 | } 171 | 172 | /** 173 | * Check upload actions 174 | * 175 | * @return bool 176 | */ 177 | protected function checkUpload() 178 | { 179 | $pathToWrite = $this->request->input('path') 180 | ? $this->request->input('path').'/' : ''; 181 | 182 | // filter 183 | $firstFall = Arr::first($this->request->file('files'), 184 | function ($value) use ($pathToWrite) { 185 | // need r/w access 186 | return $this->acl->getAccessLevel( 187 | $this->disk, 188 | $pathToWrite.$value->getClientOriginalName() 189 | ) !== 2; 190 | }, null); 191 | 192 | // if founded one access error 193 | if ($firstFall) { 194 | return false; 195 | } 196 | 197 | return true; 198 | } 199 | 200 | /** 201 | * Check delete actions 202 | * 203 | * @return bool 204 | */ 205 | protected function checkDelete() 206 | { 207 | $firstFall = Arr::first($this->request->input('items'), 208 | function ($value) { 209 | // need r/w access 210 | return $this->acl->getAccessLevel($this->disk, $value['path']) !== 2; 211 | }, null); 212 | 213 | if ($firstFall) { 214 | return false; 215 | } 216 | 217 | return true; 218 | } 219 | 220 | /** 221 | * Check paste action 222 | * 223 | * @return bool 224 | */ 225 | protected function checkPaste() 226 | { 227 | // get clipboard data 228 | $clipboard = $this->request->input('clipboard'); 229 | 230 | // copy - r, cut - rw 231 | $getLevel = $clipboard['type'] === 'copy' ? 1 : 2; 232 | 233 | // can user copy or cut selected files and folders 234 | $checkDirs = Arr::first($clipboard['directories'], 235 | function ($value) use ($clipboard, $getLevel) { 236 | return $this->acl->getAccessLevel($clipboard['disk'], $value) < $getLevel; 237 | }, null); 238 | 239 | $checkFiles = Arr::first($clipboard['files'], 240 | function ($value) use ($clipboard, $getLevel) { 241 | return $this->acl->getAccessLevel($clipboard['disk'], $value) < $getLevel; 242 | }, null); 243 | 244 | // can user write to selected folder? 245 | $writeToFolder = $this->acl->getAccessLevel($this->disk, $this->path); 246 | 247 | return ! ($checkDirs || $checkFiles || $writeToFolder !== 2); 248 | } 249 | 250 | /** 251 | * Check rename actions 252 | * 253 | * @return bool 254 | */ 255 | protected function checkRename() 256 | { 257 | // old path 258 | $oldPath = $this->request->input('oldName'); 259 | 260 | // new path 261 | $newPath = $this->request->input('newName'); 262 | 263 | // need r/w access 264 | return ! ($this->acl->getAccessLevel($this->disk, $oldPath) !== 2 265 | || $this->acl->getAccessLevel($this->disk, $newPath) !== 2); 266 | } 267 | 268 | /** 269 | * Check zip actions 270 | * 271 | * @return bool 272 | */ 273 | protected function checkZip() 274 | { 275 | // can user write to selected folder? 276 | $writeToFolder = $this->acl->getAccessLevel( 277 | $this->disk, 278 | $this->newPath( 279 | $this->request->input('path'), 280 | $this->request->input('name') 281 | ) 282 | ); 283 | 284 | // need r/w access 285 | if ($writeToFolder !== 2) { 286 | return false; 287 | } 288 | 289 | // data to zip 290 | $elements = $this->request->input('elements'); 291 | 292 | // can user read selected files and folders? 293 | $checkDirs = Arr::first($elements['directories'], 294 | function ($value) { 295 | // need r access 296 | return $this->acl->getAccessLevel($this->disk, $value) === 0; 297 | }, null); 298 | 299 | 300 | $checkFiles = Arr::first($elements['files'], 301 | function ($value) { 302 | // need r access 303 | return $this->acl->getAccessLevel($this->disk, $value) === 0; 304 | }, null); 305 | 306 | return ! ($checkDirs || $checkFiles); 307 | } 308 | 309 | /** 310 | * Check unzip actions 311 | * 312 | * @return bool 313 | */ 314 | protected function checkUnzip() 315 | { 316 | if ($this->request->input('folder')) { 317 | $dirname = dirname($this->path) === '.' ? '' 318 | : dirname($this->path).'/'; 319 | $pathToWrite = $dirname.$this->request->input('folder'); 320 | } else { 321 | $pathToWrite = dirname($this->path) === '.' ? '/' 322 | : dirname($this->path); 323 | } 324 | 325 | return ! ($this->acl->getAccessLevel($this->disk, $pathToWrite) !== 2 326 | || $this->acl->getAccessLevel($this->disk, $this->path) === 0); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/Controllers/FileManagerController.php: -------------------------------------------------------------------------------- 1 | fm = $fm; 48 | } 49 | 50 | /** 51 | * Initialize file manager 52 | * 53 | * @return JsonResponse 54 | */ 55 | public function initialize(): JsonResponse 56 | { 57 | event(new BeforeInitialization()); 58 | 59 | return response()->json( 60 | $this->fm->initialize() 61 | ); 62 | } 63 | 64 | /** 65 | * Get files and directories for the selected path and disk 66 | * 67 | * @param RequestValidator $request 68 | * 69 | * @return JsonResponse 70 | * @throws FilesystemException 71 | */ 72 | public function content(RequestValidator $request): JsonResponse 73 | { 74 | return response()->json( 75 | $this->fm->content( 76 | $request->input('disk'), 77 | $request->input('path') 78 | ) 79 | ); 80 | } 81 | 82 | /** 83 | * Directory tree 84 | * 85 | * @param RequestValidator $request 86 | * 87 | * @return JsonResponse 88 | * @throws FilesystemException 89 | */ 90 | public function tree(RequestValidator $request): JsonResponse 91 | { 92 | return response()->json( 93 | $this->fm->tree( 94 | $request->input('disk'), 95 | $request->input('path') 96 | ) 97 | ); 98 | } 99 | 100 | /** 101 | * Check the selected disk 102 | * 103 | * @param RequestValidator $request 104 | * 105 | * @return JsonResponse 106 | */ 107 | public function selectDisk(RequestValidator $request): JsonResponse 108 | { 109 | event(new DiskSelected($request->input('disk'))); 110 | 111 | return response()->json([ 112 | 'result' => [ 113 | 'status' => 'success', 114 | 'message' => 'diskSelected', 115 | ], 116 | ]); 117 | } 118 | 119 | /** 120 | * Upload files 121 | * 122 | * @param RequestValidator $request 123 | * 124 | * @return JsonResponse 125 | */ 126 | public function upload(RequestValidator $request): JsonResponse 127 | { 128 | event(new FilesUploading($request)); 129 | 130 | $uploadResponse = $this->fm->upload( 131 | $request->input('disk'), 132 | $request->input('path'), 133 | $request->file('files'), 134 | $request->input('overwrite') 135 | ); 136 | $status = $uploadResponse['result']['status']; 137 | 138 | if ($status === "success") 139 | event(new FilesUploaded($request)); 140 | else 141 | event(new FilesUploadFailed($request,$uploadResponse['result']['message'])); 142 | 143 | 144 | return response()->json($uploadResponse); 145 | } 146 | 147 | /** 148 | * Delete files and folders 149 | * 150 | * @param RequestValidator $request 151 | * 152 | * @return JsonResponse 153 | */ 154 | public function delete(RequestValidator $request): JsonResponse 155 | { 156 | event(new Deleting($request)); 157 | 158 | $deleteResponse = $this->fm->delete( 159 | $request->input('disk'), 160 | $request->input('items') 161 | ); 162 | 163 | return response()->json($deleteResponse); 164 | } 165 | 166 | /** 167 | * Copy / Cut files and folders 168 | * 169 | * @param RequestValidator $request 170 | * 171 | * @return JsonResponse 172 | */ 173 | public function paste(RequestValidator $request): JsonResponse 174 | { 175 | event(new Paste($request)); 176 | 177 | return response()->json( 178 | $this->fm->paste( 179 | $request->input('disk'), 180 | $request->input('path'), 181 | $request->input('clipboard') 182 | ) 183 | ); 184 | } 185 | 186 | /** 187 | * Rename 188 | * 189 | * @param RequestValidator $request 190 | * 191 | * @return JsonResponse 192 | */ 193 | public function rename(RequestValidator $request): JsonResponse 194 | { 195 | event(new Rename($request)); 196 | 197 | return response()->json( 198 | $this->fm->rename( 199 | $request->input('disk'), 200 | $request->input('newName'), 201 | $request->input('oldName') 202 | ) 203 | ); 204 | } 205 | 206 | /** 207 | * Download file 208 | * 209 | * @param RequestValidator $request 210 | * 211 | * @return StreamedResponse 212 | */ 213 | public function download(RequestValidator $request): StreamedResponse 214 | { 215 | event(new Download($request)); 216 | 217 | return $this->fm->download( 218 | $request->input('disk'), 219 | $request->input('path') 220 | ); 221 | } 222 | 223 | /** 224 | * Create thumbnails 225 | * 226 | * @param RequestValidator $request 227 | * 228 | * @return Response|mixed 229 | * @throws BindingResolutionException 230 | */ 231 | public function thumbnails(RequestValidator $request): mixed 232 | { 233 | return $this->fm->thumbnails( 234 | $request->input('disk'), 235 | $request->input('path') 236 | ); 237 | } 238 | 239 | /** 240 | * Image preview 241 | * 242 | * @param RequestValidator $request 243 | * 244 | * @return mixed 245 | */ 246 | public function preview(RequestValidator $request): mixed 247 | { 248 | return $this->fm->preview( 249 | $request->input('disk'), 250 | $request->input('path') 251 | ); 252 | } 253 | 254 | /** 255 | * File url 256 | * 257 | * @param RequestValidator $request 258 | * 259 | * @return JsonResponse 260 | */ 261 | public function url(RequestValidator $request): JsonResponse 262 | { 263 | return response()->json( 264 | $this->fm->url( 265 | $request->input('disk'), 266 | $request->input('path') 267 | ) 268 | ); 269 | } 270 | 271 | /** 272 | * Create new directory 273 | * 274 | * @param RequestValidator $request 275 | * 276 | * @return JsonResponse 277 | */ 278 | public function createDirectory(RequestValidator $request): JsonResponse 279 | { 280 | event(new DirectoryCreating($request)); 281 | 282 | $createDirectoryResponse = $this->fm->createDirectory( 283 | $request->input('disk'), 284 | $request->input('path'), 285 | $request->input('name') 286 | ); 287 | 288 | if ($createDirectoryResponse['result']['status'] === 'success') { 289 | event(new DirectoryCreated($request)); 290 | } 291 | 292 | return response()->json($createDirectoryResponse); 293 | } 294 | 295 | /** 296 | * Create new file 297 | * 298 | * @param RequestValidator $request 299 | * 300 | * @return JsonResponse 301 | */ 302 | public function createFile(RequestValidator $request): JsonResponse 303 | { 304 | event(new FileCreating($request)); 305 | 306 | $createFileResponse = $this->fm->createFile( 307 | $request->input('disk'), 308 | $request->input('path'), 309 | $request->input('name') 310 | ); 311 | 312 | if ($createFileResponse['result']['status'] === 'success') { 313 | event(new FileCreated($request)); 314 | } 315 | 316 | return response()->json($createFileResponse); 317 | } 318 | 319 | /** 320 | * Update file 321 | * 322 | * @param RequestValidator $request 323 | * 324 | * @return JsonResponse 325 | */ 326 | public function updateFile(RequestValidator $request): JsonResponse 327 | { 328 | event(new FileUpdate($request)); 329 | 330 | return response()->json( 331 | $this->fm->updateFile( 332 | $request->input('disk'), 333 | $request->input('path'), 334 | $request->file('file') 335 | ) 336 | ); 337 | } 338 | 339 | /** 340 | * Stream file 341 | * 342 | * @param RequestValidator $request 343 | * 344 | * @return mixed 345 | */ 346 | public function streamFile(RequestValidator $request): mixed 347 | { 348 | return $this->fm->streamFile( 349 | $request->input('disk'), 350 | $request->input('path') 351 | ); 352 | } 353 | 354 | /** 355 | * Create zip archive 356 | * 357 | * @param RequestValidator $request 358 | * @param Zip $zip 359 | * 360 | * @return array 361 | */ 362 | public function zip(RequestValidator $request, Zip $zip) 363 | { 364 | event(new ZipEvent($request)); 365 | 366 | return $zip->create(); 367 | } 368 | 369 | /** 370 | * Extract zip archive 371 | * 372 | * @param RequestValidator $request 373 | * @param Zip $zip 374 | * 375 | * @return array 376 | */ 377 | public function unzip(RequestValidator $request, Zip $zip) 378 | { 379 | event(new UnzipEvent($request)); 380 | 381 | return $zip->extract(); 382 | } 383 | 384 | /** 385 | * Integration with ckeditor 4 386 | * 387 | * @return Factory|View 388 | */ 389 | public function ckeditor(): Factory|View 390 | { 391 | return view('file-manager::ckeditor'); 392 | } 393 | 394 | /** 395 | * Integration with TinyMCE v4 396 | * 397 | * @return Factory|View 398 | */ 399 | public function tinymce(): Factory|View 400 | { 401 | return view('file-manager::tinymce'); 402 | } 403 | 404 | /** 405 | * Integration with TinyMCE v5 406 | * 407 | * @return Factory|View 408 | */ 409 | public function tinymce5(): Factory|View 410 | { 411 | return view('file-manager::tinymce5'); 412 | } 413 | 414 | /** 415 | * Integration with SummerNote 416 | * 417 | * @return Factory|View 418 | */ 419 | public function summernote(): Factory|View 420 | { 421 | return view('file-manager::summernote'); 422 | } 423 | 424 | /** 425 | * Simple integration with input field 426 | * 427 | * @return Factory|View 428 | */ 429 | public function fmButton(): Factory|View 430 | { 431 | return view('file-manager::fmButton'); 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /src/FileManager.php: -------------------------------------------------------------------------------- 1 | configRepository = $configRepository; 37 | } 38 | 39 | /** 40 | * Initialize App 41 | * 42 | * @return array 43 | */ 44 | public function initialize(): array 45 | { 46 | if (!config()->has('file-manager')) { 47 | return [ 48 | 'result' => [ 49 | 'status' => 'danger', 50 | 'message' => 'noConfig', 51 | ], 52 | ]; 53 | } 54 | 55 | $config = [ 56 | 'acl' => $this->configRepository->getAcl(), 57 | 'leftDisk' => $this->configRepository->getLeftDisk(), 58 | 'rightDisk' => $this->configRepository->getRightDisk(), 59 | 'leftPath' => $this->configRepository->getLeftPath(), 60 | 'rightPath' => $this->configRepository->getRightPath(), 61 | 'windowsConfig' => $this->configRepository->getWindowsConfig(), 62 | 'hiddenFiles' => $this->configRepository->getHiddenFiles(), 63 | ]; 64 | 65 | // disk list 66 | foreach ($this->configRepository->getDiskList() as $disk) { 67 | if (array_key_exists($disk, config('filesystems.disks'))) { 68 | $config['disks'][$disk] = Arr::only( 69 | config('filesystems.disks')[$disk], ['driver'] 70 | ); 71 | } 72 | } 73 | 74 | // get language 75 | $config['lang'] = app()->getLocale(); 76 | 77 | return [ 78 | 'result' => [ 79 | 'status' => 'success', 80 | 'message' => null, 81 | ], 82 | 'config' => $config, 83 | ]; 84 | } 85 | 86 | /** 87 | * Get files and directories for the selected path and disk 88 | * 89 | * @param $disk 90 | * @param $path 91 | * 92 | * @return array 93 | * @throws FilesystemException 94 | */ 95 | public function content($disk, $path): array 96 | { 97 | $content = $this->getContent($disk, $path); 98 | 99 | return [ 100 | 'result' => [ 101 | 'status' => 'success', 102 | 'message' => null, 103 | ], 104 | 'directories' => $content['directories'], 105 | 'files' => $content['files'], 106 | ]; 107 | } 108 | 109 | /** 110 | * Get part of the directory tree 111 | * 112 | * @param $disk 113 | * @param $path 114 | * 115 | * @return array 116 | * @throws FilesystemException 117 | */ 118 | public function tree($disk, $path): array 119 | { 120 | $directories = $this->getDirectoriesTree($disk, $path); 121 | 122 | return [ 123 | 'result' => [ 124 | 'status' => 'success', 125 | 'message' => null, 126 | ], 127 | 'directories' => $directories, 128 | ]; 129 | } 130 | 131 | /** 132 | * Upload files 133 | * 134 | * @param string|null $disk 135 | * @param string|null $path 136 | * @param array|null $files 137 | * @param bool $overwrite 138 | * 139 | * @return array 140 | */ 141 | public function upload($disk, $path, $files, $overwrite): array 142 | { 143 | $fileNotUploaded = false; 144 | 145 | foreach ($files as $file) { 146 | // skip or overwrite files 147 | if (!$overwrite && Storage::disk($disk)->exists($path . '/' . $file->getClientOriginalName())) { 148 | continue; 149 | } 150 | 151 | // check file size 152 | if ($this->configRepository->getMaxUploadFileSize() 153 | && $file->getSize() / 1024 > $this->configRepository->getMaxUploadFileSize() 154 | ) { 155 | $fileNotUploaded = true; 156 | continue; 157 | } 158 | 159 | // check file type 160 | if ($this->configRepository->getAllowFileTypes() 161 | && !in_array( 162 | $file->getClientOriginalExtension(), 163 | $this->configRepository->getAllowFileTypes() 164 | ) 165 | ) { 166 | $fileNotUploaded = true; 167 | continue; 168 | } 169 | 170 | $name = $file->getClientOriginalName(); 171 | if ($this->configRepository->getSlugifyNames()) { 172 | $name = Str::slug( 173 | Str::replace( 174 | '.' . $file->getClientOriginalExtension(), 175 | '', 176 | $name 177 | ) 178 | ) . '.' . $file->getClientOriginalExtension(); 179 | } 180 | // overwrite or save file 181 | Storage::disk($disk)->putFileAs( 182 | $path, 183 | $file, 184 | $name 185 | ); 186 | } 187 | 188 | if ($fileNotUploaded) { 189 | return [ 190 | 'result' => [ 191 | 'status' => 'warning', 192 | 'message' => 'notAllUploaded', 193 | ], 194 | ]; 195 | } 196 | 197 | return [ 198 | 'result' => [ 199 | 'status' => 'success', 200 | 'message' => 'uploaded', 201 | ], 202 | ]; 203 | } 204 | 205 | /** 206 | * Delete files and folders 207 | * 208 | * @param $disk 209 | * @param $items 210 | * 211 | * @return array 212 | */ 213 | public function delete($disk, $items): array 214 | { 215 | $deletedItems = []; 216 | 217 | foreach ($items as $item) { 218 | if (!Storage::disk($disk)->exists($item['path'])) { 219 | continue; 220 | } else { 221 | if ($item['type'] === 'dir') { 222 | Storage::disk($disk)->deleteDirectory($item['path']); 223 | } else { 224 | Storage::disk($disk)->delete($item['path']); 225 | } 226 | } 227 | 228 | $deletedItems[] = $item; 229 | } 230 | 231 | event(new Deleted($disk, $deletedItems)); 232 | 233 | return [ 234 | 'result' => [ 235 | 'status' => 'success', 236 | 'message' => 'deleted', 237 | ], 238 | ]; 239 | } 240 | 241 | /** 242 | * Copy / Cut - Files and Directories 243 | * 244 | * @param $disk 245 | * @param $path 246 | * @param $clipboard 247 | * 248 | * @return array 249 | */ 250 | public function paste($disk, $path, $clipboard): array 251 | { 252 | // compare disk names 253 | if ($disk !== $clipboard['disk']) { 254 | 255 | if (!$this->checkDisk($clipboard['disk'])) { 256 | return $this->notFoundMessage(); 257 | } 258 | } 259 | 260 | $transferService = TransferFactory::build($disk, $path, $clipboard); 261 | 262 | return $transferService->filesTransfer(); 263 | } 264 | 265 | /** 266 | * Rename file or folder 267 | * 268 | * @param $disk 269 | * @param $newName 270 | * @param $oldName 271 | * 272 | * @return array 273 | */ 274 | public function rename($disk, $newName, $oldName): array 275 | { 276 | Storage::disk($disk)->move($oldName, $newName); 277 | 278 | return [ 279 | 'result' => [ 280 | 'status' => 'success', 281 | 'message' => 'renamed', 282 | ], 283 | ]; 284 | } 285 | 286 | /** 287 | * Download selected file 288 | * 289 | * @param $disk 290 | * @param $path 291 | * 292 | * @return StreamedResponse 293 | */ 294 | public function download($disk, $path): StreamedResponse 295 | { 296 | // if file name not in ASCII format 297 | if (!preg_match('/^[\x20-\x7e]*$/', basename($path))) { 298 | $filename = Str::ascii(basename($path)); 299 | } else { 300 | $filename = basename($path); 301 | } 302 | 303 | return Storage::disk($disk)->download($path, $filename); 304 | } 305 | 306 | /** 307 | * Create thumbnails 308 | * 309 | * @param $disk 310 | * @param $path 311 | * 312 | * @return Response|mixed 313 | * @throws BindingResolutionException 314 | */ 315 | public function thumbnails($disk, $path): mixed 316 | { 317 | return response()->make( 318 | Image::read( 319 | Storage::disk($disk)->get($path)) 320 | ->coverDown(80, 80) 321 | ->encode(), 322 | 200, 323 | ['Content-Type' => Storage::disk($disk)->mimeType($path)] 324 | ); 325 | } 326 | 327 | /** 328 | * Image preview 329 | * 330 | * @param $disk 331 | * @param $path 332 | * 333 | * @return mixed 334 | * @throws BindingResolutionException 335 | */ 336 | public function preview($disk, $path): mixed 337 | { 338 | return response()->make( 339 | Image::read(Storage::disk($disk)->get($path))->encode(), 340 | 200, 341 | ['Content-Type' => Storage::disk($disk)->mimeType($path)] 342 | ); 343 | } 344 | 345 | /** 346 | * Get file URL 347 | * 348 | * @param $disk 349 | * @param $path 350 | * 351 | * @return array 352 | */ 353 | public function url($disk, $path): array 354 | { 355 | return [ 356 | 'result' => [ 357 | 'status' => 'success', 358 | 'message' => null, 359 | ], 360 | 'url' => Storage::disk($disk)->url($path), 361 | ]; 362 | } 363 | 364 | /** 365 | * Create new directory 366 | * 367 | * @param $disk 368 | * @param $path 369 | * @param $name 370 | * 371 | * @return array 372 | */ 373 | public function createDirectory($disk, $path, $name) 374 | { 375 | $directoryName = $this->newPath($path, $name); 376 | 377 | if (Storage::disk($disk)->exists($directoryName)) { 378 | return [ 379 | 'result' => [ 380 | 'status' => 'warning', 381 | 'message' => 'dirExist', 382 | ], 383 | ]; 384 | } 385 | 386 | Storage::disk($disk)->makeDirectory($directoryName); 387 | $directoryProperties = $this->directoryProperties( 388 | $disk, 389 | $directoryName 390 | ); 391 | 392 | // add directory properties for the tree module 393 | $tree = $directoryProperties; 394 | $tree['props'] = ['hasSubdirectories' => false]; 395 | 396 | return [ 397 | 'result' => [ 398 | 'status' => 'success', 399 | 'message' => 'dirCreated', 400 | ], 401 | 'directory' => $directoryProperties, 402 | 'tree' => [$tree], 403 | ]; 404 | } 405 | 406 | /** 407 | * Create new file 408 | * 409 | * @param $disk 410 | * @param $path 411 | * @param $name 412 | * 413 | * @return array 414 | */ 415 | public function createFile($disk, $path, $name): array 416 | { 417 | $path = $this->newPath($path, $name); 418 | 419 | if (Storage::disk($disk)->exists($path)) { 420 | return [ 421 | 'result' => [ 422 | 'status' => 'warning', 423 | 'message' => 'fileExist', 424 | ], 425 | ]; 426 | } 427 | 428 | Storage::disk($disk)->put($path, ''); 429 | $fileProperties = $this->fileProperties($disk, $path); 430 | 431 | return [ 432 | 'result' => [ 433 | 'status' => 'success', 434 | 'message' => 'fileCreated', 435 | ], 436 | 'file' => $fileProperties, 437 | ]; 438 | } 439 | 440 | /** 441 | * Update file 442 | * 443 | * @param $disk 444 | * @param $path 445 | * @param $file 446 | * 447 | * @return array 448 | */ 449 | public function updateFile($disk, $path, $file): array 450 | { 451 | Storage::disk($disk)->putFileAs( 452 | $path, 453 | $file, 454 | $file->getClientOriginalName() 455 | ); 456 | 457 | $filePath = $this->newPath($path, $file->getClientOriginalName()); 458 | $fileProperties = $this->fileProperties($disk, $filePath); 459 | 460 | return [ 461 | 'result' => [ 462 | 'status' => 'success', 463 | 'message' => 'fileUpdated', 464 | ], 465 | 'file' => $fileProperties, 466 | ]; 467 | } 468 | 469 | /** 470 | * Stream file - for audio and video 471 | * 472 | * @param $disk 473 | * @param $path 474 | * 475 | * @return StreamedResponse 476 | */ 477 | public function streamFile($disk, $path): StreamedResponse 478 | { 479 | // if file name not in ASCII format 480 | if (!preg_match('/^[\x20-\x7e]*$/', basename($path))) { 481 | $filename = Str::ascii(basename($path)); 482 | } else { 483 | $filename = basename($path); 484 | } 485 | 486 | return Storage::disk($disk)->response($path, $filename, ['Accept-Ranges' => 'bytes']); 487 | } 488 | } 489 | -------------------------------------------------------------------------------- /resources/assets/css/file-manager.css: -------------------------------------------------------------------------------- 1 | .fm-navbar{flex:0 0 auto}.fm-navbar .col-auto>.btn-group:not(:last-child){margin-right:.4rem}.fm-tree-branch{display:table;width:100%;padding-left:1rem}.fm-tree-branch li>p{margin-bottom:0;padding:.4rem;white-space:nowrap;cursor:pointer}.fm-tree-branch li>p:hover,.fm-tree-branch li>p.selected{background-color:#f8f9fa}.fm-tree-branch .bi.bi-dash,.fm-tree-branch .bi.bi-dash-square,.fm-tree-branch .bi.bi-plus-square{font-size:.9rem;padding-right:.4rem}.fade-tree-enter-active,.fade-tree-leave-active{transition:all .3s ease}.fade-tree-enter,.fade-tree-leave-to{transform:translate(20px);opacity:0}.fm-tree{overflow:auto;border-right:1px solid #6c757d}.fm-tree>.fm-tree-branch{padding-left:0}.fm-tree .fm-tree-disk{padding:.2rem .3rem;background-color:#cff4fc}.fm-tree .fm-tree-disk>i{padding-right:.4rem}.fm-disk-list ul.list-inline{margin-bottom:.5rem}.fm-disk-list .badge.bg-light{cursor:pointer}.fm-breadcrumb .breadcrumb{flex-wrap:nowrap;padding:.2rem .3rem;margin-bottom:.5rem}.fm-breadcrumb .breadcrumb.active-manager{background-color:#cff4fc}.fm-breadcrumb .breadcrumb .breadcrumb-item:not(.active):hover{cursor:pointer;font-weight:400;color:#6c757d}.fm-table thead th{background:white;position:sticky;top:0;z-index:10;cursor:pointer;border-top:none}.fm-table thead th:hover{background-color:#f8f9fa}.fm-table thead th>i{padding-left:.5rem}.fm-table td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.fm-table tr:hover{background-color:#f8f9fa}.fm-table .w-10{width:10%}.fm-table .w-65{width:65%}.fm-table .fm-content-item{cursor:pointer;max-width:1px}.fm-table .text-hidden{color:#cdcdcd}.fm-thumbnail{display:flex;justify-content:center;align-items:center}.fm-thumbnail .img-thumbnail{width:88px;height:88px}.fm-thumbnail .fade-enter-active,.fm-thumbnail .fade-leave-active{transition:opacity .3s}.fm-thumbnail .fade-enter,.fm-thumbnail .fade-leave-to{opacity:0}.fm-grid{padding-top:1rem}.fm-grid .fm-grid-item{position:relative;width:125px;padding:.4rem;margin-bottom:1rem;margin-right:1rem;border-radius:5px}.fm-grid .fm-grid-item.active{background-color:#cff4fc;box-shadow:3px 2px 5px gray}.fm-grid .fm-grid-item:not(.active):hover{background-color:#f8f9fa;box-shadow:3px 2px 5px gray}.fm-grid .fm-grid-item .fm-item-icon{font-size:5rem;cursor:pointer}.fm-grid .fm-grid-item .fm-item-icon>i,.fm-grid .fm-grid-item .fm-item-icon>figure>i{color:#6c757d}.fm-grid .fm-grid-item .fm-item-info{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fm-content{padding-left:1rem}.fm-content .fm-content-body{overflow:auto}.fm-modal-upload .fm-btn-wrapper{position:relative;overflow:hidden;padding-bottom:6px;margin-bottom:.6rem}.fm-modal-upload .fm-btn-wrapper input[type=file]{font-size:100px;position:absolute;left:0;top:0;opacity:0;cursor:pointer}.fm-modal-upload .fm-upload-list .bi{padding-right:.5rem}.fm-modal-upload .fm-upload-list .form-check-inline{margin-right:0}.fm-modal-upload .fm-upload-info>.progress{margin-bottom:1rem}.fm-additions-file-list .bi,.fm-modal-clipboard .modal-body .far{padding-right:.5rem}.fm-modal-properties .modal-body .row{margin-bottom:.3rem;padding-top:.3rem;padding-bottom:.3rem}.fm-modal-properties .modal-body .row .bi-files{display:none;cursor:pointer}.fm-modal-properties .modal-body .row:hover{background-color:#f8f9fa}.fm-modal-properties .modal-body .row:hover .bi-files{display:block}.fm-modal-properties .modal-body .col-2{font-weight:700}.fm-modal-properties .modal-body .col-9{word-wrap:break-word}/*! 2 | * Cropper.js v1.6.1 3 | * https://fengyuanchen.github.io/cropperjs 4 | * 5 | * Copyright 2015-present Chen Fengyuan 6 | * Released under the MIT license 7 | * 8 | * Date: 2023-09-17T03:44:17.565Z 9 | */.cropper-container{direction:ltr;font-size:0;line-height:0;position:relative;-ms-touch-action:none;touch-action:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.cropper-container img{backface-visibility:hidden;display:block;height:100%;image-orientation:0deg;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;width:100%}.cropper-wrap-box,.cropper-canvas,.cropper-drag-box,.cropper-crop-box,.cropper-modal{bottom:0;left:0;position:absolute;right:0;top:0}.cropper-wrap-box,.cropper-canvas{overflow:hidden}.cropper-drag-box{background-color:#fff;opacity:0}.cropper-modal{background-color:#000;opacity:.5}.cropper-view-box{display:block;height:100%;outline:1px solid #39f;outline-color:#3399ffbf;overflow:hidden;width:100%}.cropper-dashed{border:0 dashed #eee;display:block;opacity:.5;position:absolute}.cropper-dashed.dashed-h{border-bottom-width:1px;border-top-width:1px;height:calc(100% / 3);left:0;top:calc(100% / 3);width:100%}.cropper-dashed.dashed-v{border-left-width:1px;border-right-width:1px;height:100%;left:calc(100% / 3);top:0;width:calc(100% / 3)}.cropper-center{display:block;height:0;left:50%;opacity:.75;position:absolute;top:50%;width:0}.cropper-center:before,.cropper-center:after{background-color:#eee;content:" ";display:block;position:absolute}.cropper-center:before{height:1px;left:-3px;top:0;width:7px}.cropper-center:after{height:7px;left:0;top:-3px;width:1px}.cropper-face,.cropper-line,.cropper-point{display:block;height:100%;opacity:.1;position:absolute;width:100%}.cropper-face{background-color:#fff;left:0;top:0}.cropper-line{background-color:#39f}.cropper-line.line-e{cursor:ew-resize;right:-3px;top:0;width:5px}.cropper-line.line-n{cursor:ns-resize;height:5px;left:0;top:-3px}.cropper-line.line-w{cursor:ew-resize;left:-3px;top:0;width:5px}.cropper-line.line-s{bottom:-3px;cursor:ns-resize;height:5px;left:0}.cropper-point{background-color:#39f;height:5px;opacity:.75;width:5px}.cropper-point.point-e{cursor:ew-resize;margin-top:-3px;right:-3px;top:50%}.cropper-point.point-n{cursor:ns-resize;left:50%;margin-left:-3px;top:-3px}.cropper-point.point-w{cursor:ew-resize;left:-3px;margin-top:-3px;top:50%}.cropper-point.point-s{bottom:-3px;cursor:s-resize;left:50%;margin-left:-3px}.cropper-point.point-ne{cursor:nesw-resize;right:-3px;top:-3px}.cropper-point.point-nw{cursor:nwse-resize;left:-3px;top:-3px}.cropper-point.point-sw{bottom:-3px;cursor:nesw-resize;left:-3px}.cropper-point.point-se{bottom:-3px;cursor:nwse-resize;height:20px;opacity:1;right:-3px;width:20px}@media (min-width: 768px){.cropper-point.point-se{height:15px;width:15px}}@media (min-width: 992px){.cropper-point.point-se{height:10px;width:10px}}@media (min-width: 1200px){.cropper-point.point-se{height:5px;opacity:.75;width:5px}}.cropper-point.point-se:before{background-color:#39f;bottom:-50%;content:" ";display:block;height:200%;opacity:0;position:absolute;right:-50%;width:200%}.cropper-invisible{opacity:0}.cropper-bg{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC)}.cropper-hide{display:block;height:0;position:absolute;width:0}.cropper-hidden{display:none!important}.cropper-move{cursor:move}.cropper-crop{cursor:crosshair}.cropper-disabled .cropper-drag-box,.cropper-disabled .cropper-face,.cropper-disabled .cropper-line,.cropper-disabled .cropper-point{cursor:not-allowed}.fm-additions-cropper{overflow:hidden}.fm-additions-cropper button>i{color:#fff;font-weight:700}.fm-additions-cropper>.row{flex-wrap:nowrap}.fm-additions-cropper .cropper-block{overflow:hidden}.fm-additions-cropper .cropper-block img{max-width:100%}.fm-additions-cropper .col-sm-3{overflow:auto}.fm-additions-cropper .col-sm-3::-webkit-scrollbar{display:none}.fm-additions-cropper .cropper-preview{margin-bottom:1rem;overflow:hidden;height:200px}.fm-additions-cropper .cropper-preview img{max-width:100%}.fm-additions-cropper .cropper-data{padding-left:1rem;padding-right:1rem}.fm-additions-cropper .cropper-data>.input-group{margin-bottom:.5rem}.fm-additions-cropper .cropper-data .input-group>.input-group-text:first-child{min-width:4rem}.fm-additions-cropper .cropper-data .input-group>.input-group-text:last-child{min-width:3rem}.fm-additions-cropper>.d-flex{padding:1rem;border-top:1px solid #e9ecef}.fm-modal-preview .modal-body{padding:0}.fm-modal-preview .modal-body img{max-width:100%}.fm-modal-preview>.d-flex{padding:1rem;border-top:1px solid #e9ecef}.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid black;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor .CodeMirror-line::selection,.cm-fat-cursor .CodeMirror-line>span::selection,.cm-fat-cursor .CodeMirror-line>span>span::selection{background:transparent}.cm-fat-cursor .CodeMirror-line::-moz-selection,.cm-fat-cursor .CodeMirror-line>span::-moz-selection,.cm-fat-cursor .CodeMirror-line>span>span::-moz-selection{background:transparent}.cm-fat-cursor{caret-color:transparent}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3,.cm-s-default .cm-type{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error,.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:white}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:none;position:relative;z-index:0}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-vscrollbar,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-gutter-filler{position:absolute;z-index:6;display:none;outline:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:none!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:transparent;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:none}.CodeMirror-scroll,.CodeMirror-sizer,.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors,.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:#ff06}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:""}span.CodeMirror-selectedtext{background:none}.CodeMirror-merge{position:relative;border:1px solid #ddd;white-space:pre}.CodeMirror-merge,.CodeMirror-merge .CodeMirror{height:350px}.CodeMirror-merge-2pane .CodeMirror-merge-pane{width:47%}.CodeMirror-merge-2pane .CodeMirror-merge-gap{width:6%}.CodeMirror-merge-3pane .CodeMirror-merge-pane{width:31%}.CodeMirror-merge-3pane .CodeMirror-merge-gap{width:3.5%}.CodeMirror-merge-pane{display:inline-block;white-space:normal;vertical-align:top}.CodeMirror-merge-pane-rightmost{position:absolute;right:0px;z-index:1}.CodeMirror-merge-gap{z-index:2;display:inline-block;height:100%;-moz-box-sizing:border-box;box-sizing:border-box;overflow:hidden;border-left:1px solid #ddd;border-right:1px solid #ddd;position:relative;background:#f8f8f8}.CodeMirror-merge-scrolllock-wrap{position:absolute;bottom:0;left:50%}.CodeMirror-merge-scrolllock{position:relative;left:-50%;cursor:pointer;color:#555;line-height:1}.CodeMirror-merge-scrolllock:after{content:"\21db\a0\a0\21da"}.CodeMirror-merge-scrolllock.CodeMirror-merge-scrolllock-enabled:after{content:"\21db\21da"}.CodeMirror-merge-copybuttons-left,.CodeMirror-merge-copybuttons-right{position:absolute;left:0;top:0;right:0;bottom:0;line-height:1}.CodeMirror-merge-copy{position:absolute;cursor:pointer;color:#44c;z-index:3}.CodeMirror-merge-copy-reverse{position:absolute;cursor:pointer;color:#44c}.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy{left:2px}.CodeMirror-merge-copybuttons-right .CodeMirror-merge-copy{right:2px}.CodeMirror-merge-r-inserted,.CodeMirror-merge-l-inserted{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12MwuCXy3+CWyH8GBgYGJgYkAABZbAQ9ELXurwAAAABJRU5ErkJggg==);background-position:bottom left;background-repeat:repeat-x}.CodeMirror-merge-r-deleted,.CodeMirror-merge-l-deleted{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12M4Kyb2/6yY2H8GBgYGJgYkAABURgPz6Ks7wQAAAABJRU5ErkJggg==);background-position:bottom left;background-repeat:repeat-x}.CodeMirror-merge-r-chunk{background:#ffffe0}.CodeMirror-merge-r-chunk-start{border-top:1px solid #ee8}.CodeMirror-merge-r-chunk-end{border-bottom:1px solid #ee8}.CodeMirror-merge-r-connect{fill:#ffffe0;stroke:#ee8;stroke-width:1px}.CodeMirror-merge-l-chunk{background:#eef}.CodeMirror-merge-l-chunk-start{border-top:1px solid #88e}.CodeMirror-merge-l-chunk-end{border-bottom:1px solid #88e}.CodeMirror-merge-l-connect{fill:#eef;stroke:#88e;stroke-width:1px}.CodeMirror-merge-l-chunk.CodeMirror-merge-r-chunk{background:#dfd}.CodeMirror-merge-l-chunk-start.CodeMirror-merge-r-chunk-start{border-top:1px solid #4e4}.CodeMirror-merge-l-chunk-end.CodeMirror-merge-r-chunk-end{border-bottom:1px solid #4e4}.CodeMirror-merge-collapsed-widget:before{content:"(...)"}.CodeMirror-merge-collapsed-widget{cursor:pointer;color:#88b;background:#eef;border:1px solid #ddf;font-size:90%;padding:0 3px;border-radius:4px}.CodeMirror-merge-collapsed-line .CodeMirror-gutter-elt{display:none}.cm-s-blackboard.CodeMirror{background:#0C1021;color:#f8f8f8}.cm-s-blackboard div.CodeMirror-selected{background:#253B76}.cm-s-blackboard .CodeMirror-line::selection,.cm-s-blackboard .CodeMirror-line>span::selection,.cm-s-blackboard .CodeMirror-line>span>span::selection{background:rgba(37,59,118,.99)}.cm-s-blackboard .CodeMirror-line::-moz-selection,.cm-s-blackboard .CodeMirror-line>span::-moz-selection,.cm-s-blackboard .CodeMirror-line>span>span::-moz-selection{background:rgba(37,59,118,.99)}.cm-s-blackboard .CodeMirror-gutters{background:#0C1021;border-right:0}.cm-s-blackboard .CodeMirror-guttermarker{color:#fbde2d}.cm-s-blackboard .CodeMirror-guttermarker-subtle,.cm-s-blackboard .CodeMirror-linenumber{color:#888}.cm-s-blackboard .CodeMirror-cursor{border-left:1px solid #A7A7A7}.cm-s-blackboard .cm-keyword{color:#fbde2d}.cm-s-blackboard .cm-atom,.cm-s-blackboard .cm-number{color:#d8fa3c}.cm-s-blackboard .cm-def{color:#8da6ce}.cm-s-blackboard .cm-variable{color:#ff6400}.cm-s-blackboard .cm-operator{color:#fbde2d}.cm-s-blackboard .cm-comment{color:#aeaeae}.cm-s-blackboard .cm-string,.cm-s-blackboard .cm-string-2{color:#61ce3c}.cm-s-blackboard .cm-meta{color:#d8fa3c}.cm-s-blackboard .cm-builtin,.cm-s-blackboard .cm-tag,.cm-s-blackboard .cm-attribute{color:#8da6ce}.cm-s-blackboard .cm-header{color:#ff6400}.cm-s-blackboard .cm-hr{color:#aeaeae}.cm-s-blackboard .cm-link{color:#8da6ce}.cm-s-blackboard .cm-error{background:#9D1E15;color:#f8f8f8}.cm-s-blackboard .CodeMirror-activeline-background{background:#3C3636}.cm-s-blackboard .CodeMirror-matchingbracket{outline:1px solid grey;color:#fff!important}.fm-modal-text-edit .modal-body{padding:0}.fm-modal-audio-player .bi.bi-play-fill{color:gray;opacity:.1;cursor:pointer}.fm-modal-audio-player .bi.bi-play-fill:hover{opacity:.5}.fm-modal-audio-player .bi.bi-play-fill.active{opacity:1;color:#00bfff}.fm-modal-audio-player .bi.bi-pause-fill{color:gray;opacity:.5;cursor:pointer}@keyframes plyr-progress{to{background-position:var(--plyr-progress-loading-size, 25px) 0}}@keyframes plyr-popup{0%{opacity:.5;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes plyr-fade-in{0%{opacity:0}to{opacity:1}}.plyr{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;align-items:center;direction:ltr;display:flex;flex-direction:column;font-family:var(--plyr-font-family, inherit);font-variant-numeric:tabular-nums;font-weight:var(--plyr-font-weight-regular, 400);line-height:var(--plyr-line-height, 1.7);max-width:100%;min-width:200px;position:relative;text-shadow:none;transition:box-shadow .3s ease;z-index:0}.plyr video,.plyr audio,.plyr iframe{display:block;height:100%;width:100%}.plyr button{font:inherit;line-height:inherit;width:auto}.plyr:focus{outline:0}.plyr--full-ui{box-sizing:border-box}.plyr--full-ui *,.plyr--full-ui *:after,.plyr--full-ui *:before{box-sizing:inherit}.plyr--full-ui a,.plyr--full-ui button,.plyr--full-ui input,.plyr--full-ui label{touch-action:manipulation}.plyr__badge{background:var(--plyr-badge-background, hsl(216, 15%, 34%));border-radius:var(--plyr-badge-border-radius, 2px);color:var(--plyr-badge-text-color, #fff);font-size:var(--plyr-font-size-badge, 9px);line-height:1;padding:3px 4px}.plyr--full-ui ::-webkit-media-text-track-container{display:none}.plyr__captions{animation:plyr-fade-in .3s ease;bottom:0;display:none;font-size:var(--plyr-font-size-small, 13px);left:0;padding:var(--plyr-control-spacing, 10px);position:absolute;text-align:center;transition:transform .4s ease-in-out;width:100%}.plyr__captions span:empty{display:none}@media (min-width: 480px){.plyr__captions{font-size:var(--plyr-font-size-base, 15px);padding:calc(var(--plyr-control-spacing, 10px) * 2)}}@media (min-width: 768px){.plyr__captions{font-size:var(--plyr-font-size-large, 18px)}}.plyr--captions-active .plyr__captions{display:block}.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty)~.plyr__captions{transform:translateY(calc(var(--plyr-control-spacing, 10px) * -4))}.plyr__caption{background:var(--plyr-captions-background, rgba(0, 0, 0, .8));border-radius:2px;box-decoration-break:clone;color:var(--plyr-captions-text-color, #fff);line-height:185%;padding:.2em .5em;white-space:pre-wrap}.plyr__caption div{display:inline}.plyr__control{background:transparent;border:0;border-radius:var(--plyr-control-radius, 4px);color:inherit;cursor:pointer;flex-shrink:0;overflow:visible;padding:calc(var(--plyr-control-spacing, 10px) * .7);position:relative;transition:all .3s ease}.plyr__control svg{display:block;fill:currentColor;height:var(--plyr-control-icon-size, 18px);pointer-events:none;width:var(--plyr-control-icon-size, 18px)}.plyr__control:focus{outline:0}.plyr__control:focus-visible{outline:2px dashed var(--plyr-focus-visible-color, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));outline-offset:2px}a.plyr__control{text-decoration:none}a.plyr__control:after,a.plyr__control:before{display:none}.plyr__control:not(.plyr__control--pressed) .icon--pressed,.plyr__control.plyr__control--pressed .icon--not-pressed,.plyr__control:not(.plyr__control--pressed) .label--pressed,.plyr__control.plyr__control--pressed .label--not-pressed{display:none}.plyr--full-ui ::-webkit-media-controls{display:none}.plyr__controls{align-items:center;display:flex;justify-content:flex-end;text-align:center}.plyr__controls .plyr__progress__container{flex:1;min-width:0}.plyr__controls .plyr__controls__item{margin-left:calc(var(--plyr-control-spacing, 10px) / 4)}.plyr__controls .plyr__controls__item:first-child{margin-left:0;margin-right:auto}.plyr__controls .plyr__controls__item.plyr__progress__container{padding-left:calc(var(--plyr-control-spacing, 10px) / 4)}.plyr__controls .plyr__controls__item.plyr__time{padding:0 calc(var(--plyr-control-spacing, 10px) / 2)}.plyr__controls .plyr__controls__item.plyr__progress__container:first-child,.plyr__controls .plyr__controls__item.plyr__time:first-child,.plyr__controls .plyr__controls__item.plyr__time+.plyr__time{padding-left:0}.plyr__controls:empty{display:none}.plyr [data-plyr=captions],.plyr [data-plyr=pip],.plyr [data-plyr=airplay],.plyr [data-plyr=fullscreen]{display:none}.plyr--captions-enabled [data-plyr=captions],.plyr--pip-supported [data-plyr=pip],.plyr--airplay-supported [data-plyr=airplay],.plyr--fullscreen-enabled [data-plyr=fullscreen]{display:inline-block}.plyr__menu{display:flex;position:relative}.plyr__menu .plyr__control svg{transition:transform .3s ease}.plyr__menu .plyr__control[aria-expanded=true] svg{transform:rotate(90deg)}.plyr__menu .plyr__control[aria-expanded=true] .plyr__tooltip{display:none}.plyr__menu__container{animation:plyr-popup .2s ease;background:var(--plyr-menu-background, rgba(255, 255, 255, .9));border-radius:var(--plyr-menu-radius, 8px);bottom:100%;box-shadow:var(--plyr-menu-shadow, 0 1px 2px rgba(0, 0, 0, .15));color:var(--plyr-menu-color, hsl(216, 15%, 34%));font-size:var(--plyr-font-size-base, 15px);margin-bottom:10px;position:absolute;right:-3px;text-align:left;white-space:nowrap;z-index:3}.plyr__menu__container>div{overflow:hidden;transition:height .35s cubic-bezier(.4,0,.2,1),width .35s cubic-bezier(.4,0,.2,1)}.plyr__menu__container:after{border:var(--plyr-menu-arrow-size, 4px) solid transparent;border-top-color:var(--plyr-menu-background, rgba(255, 255, 255, .9));content:"";height:0;position:absolute;right:calc(var(--plyr-control-icon-size, 18px) / 2 + calc(var(--plyr-control-spacing, 10px) * .7) - var(--plyr-menu-arrow-size, 4px) / 2);top:100%;width:0}.plyr__menu__container [role=menu]{padding:calc(var(--plyr-control-spacing, 10px) * .7)}.plyr__menu__container [role=menuitem],.plyr__menu__container [role=menuitemradio]{margin-top:2px}.plyr__menu__container [role=menuitem]:first-child,.plyr__menu__container [role=menuitemradio]:first-child{margin-top:0}.plyr__menu__container .plyr__control{align-items:center;color:var(--plyr-menu-color, hsl(216, 15%, 34%));display:flex;font-size:var(--plyr-font-size-menu, var(--plyr-font-size-small, 13px));padding:calc(calc(var(--plyr-control-spacing, 10px) * .7) / 1.5) calc(calc(var(--plyr-control-spacing, 10px) * .7) * 1.5);user-select:none;width:100%}.plyr__menu__container .plyr__control>span{align-items:inherit;display:flex;width:100%}.plyr__menu__container .plyr__control:after{border:var(--plyr-menu-item-arrow-size, 4px) solid transparent;content:"";position:absolute;top:50%;transform:translateY(-50%)}.plyr__menu__container .plyr__control--forward{padding-right:calc(calc(var(--plyr-control-spacing, 10px) * .7) * 4)}.plyr__menu__container .plyr__control--forward:after{border-left-color:var(--plyr-menu-arrow-color, hsl(216, 15%, 52%));right:calc(calc(var(--plyr-control-spacing, 10px) * .7) * 1.5 - var(--plyr-menu-item-arrow-size, 4px))}.plyr__menu__container .plyr__control--forward:focus-visible:after,.plyr__menu__container .plyr__control--forward:hover:after{border-left-color:currentColor}.plyr__menu__container .plyr__control--back{font-weight:var(--plyr-font-weight-regular, 400);margin:calc(var(--plyr-control-spacing, 10px) * .7);margin-bottom:calc(calc(var(--plyr-control-spacing, 10px) * .7) / 2);padding-left:calc(calc(var(--plyr-control-spacing, 10px) * .7) * 4);position:relative;width:calc(100% - calc(var(--plyr-control-spacing, 10px) * .7) * 2)}.plyr__menu__container .plyr__control--back:after{border-right-color:var(--plyr-menu-arrow-color, hsl(216, 15%, 52%));left:calc(calc(var(--plyr-control-spacing, 10px) * .7) * 1.5 - var(--plyr-menu-item-arrow-size, 4px))}.plyr__menu__container .plyr__control--back:before{background:var(--plyr-menu-back-border-color, hsl(216, 15%, 88%));box-shadow:0 1px 0 var(--plyr-menu-back-border-shadow-color, #fff);content:"";height:1px;left:0;margin-top:calc(calc(var(--plyr-control-spacing, 10px) * .7) / 2);overflow:hidden;position:absolute;right:0;top:100%}.plyr__menu__container .plyr__control--back:focus-visible:after,.plyr__menu__container .plyr__control--back:hover:after{border-right-color:currentColor}.plyr__menu__container .plyr__control[role=menuitemradio]{padding-left:calc(var(--plyr-control-spacing, 10px) * .7)}.plyr__menu__container .plyr__control[role=menuitemradio]:before,.plyr__menu__container .plyr__control[role=menuitemradio]:after{border-radius:100%}.plyr__menu__container .plyr__control[role=menuitemradio]:before{background:rgba(0,0,0,.1);content:"";display:block;flex-shrink:0;height:16px;margin-right:var(--plyr-control-spacing, 10px);transition:all .3s ease;width:16px}.plyr__menu__container .plyr__control[role=menuitemradio]:after{background:#fff;border:0;height:6px;left:12px;opacity:0;top:50%;transform:translateY(-50%) scale(0);transition:transform .3s ease,opacity .3s ease;width:6px}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:before{background:var(--plyr-control-toggle-checked-background, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))))}.plyr__menu__container .plyr__control[role=menuitemradio][aria-checked=true]:after{opacity:1;transform:translateY(-50%) scale(1)}.plyr__menu__container .plyr__control[role=menuitemradio]:focus-visible:before,.plyr__menu__container .plyr__control[role=menuitemradio]:hover:before{background:rgba(35,40,47,.1)}.plyr__menu__container .plyr__menu__value{align-items:center;display:flex;margin-left:auto;margin-right:calc((calc(var(--plyr-control-spacing, 10px) * .7) - 2px) * -1);overflow:hidden;padding-left:calc(calc(var(--plyr-control-spacing, 10px) * .7) * 3.5);pointer-events:none}.plyr--full-ui input[type=range]{appearance:none;background:transparent;border:0;border-radius:calc(var(--plyr-range-thumb-height, 13px) * 2);color:var(--plyr-range-fill-background, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));display:block;height:calc(var(--plyr-range-thumb-active-shadow-width, 3px) * 2 + var(--plyr-range-thumb-height, 13px));margin:0;min-width:0;padding:0;transition:box-shadow .3s ease;width:100%}.plyr--full-ui input[type=range]::-webkit-slider-runnable-track{background:transparent;border:0;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px);transition:box-shadow .3s ease;user-select:none;background-image:linear-gradient(to right,currentColor var(--value, 0%),transparent var(--value, 0%))}.plyr--full-ui input[type=range]::-webkit-slider-thumb{background:var(--plyr-range-thumb-background, #fff);border:0;border-radius:100%;box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2));height:var(--plyr-range-thumb-height, 13px);position:relative;transition:all .2s ease;width:var(--plyr-range-thumb-height, 13px);appearance:none;margin-top:calc((var(--plyr-range-thumb-height, 13px) - var(--plyr-range-track-height, 5px)) / 2 * -1)}.plyr--full-ui input[type=range]::-moz-range-track{background:transparent;border:0;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px);transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-moz-range-thumb{background:var(--plyr-range-thumb-background, #fff);border:0;border-radius:100%;box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2));height:var(--plyr-range-thumb-height, 13px);position:relative;transition:all .2s ease;width:var(--plyr-range-thumb-height, 13px)}.plyr--full-ui input[type=range]::-moz-range-progress{background:currentColor;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px)}.plyr--full-ui input[type=range]::-ms-track{background:transparent;border:0;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px);transition:box-shadow .3s ease;user-select:none;color:transparent}.plyr--full-ui input[type=range]::-ms-fill-upper{background:transparent;border:0;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px);transition:box-shadow .3s ease;user-select:none}.plyr--full-ui input[type=range]::-ms-fill-lower{background:transparent;border:0;border-radius:calc(var(--plyr-range-track-height, 5px) / 2);height:var(--plyr-range-track-height, 5px);transition:box-shadow .3s ease;user-select:none;background:currentColor}.plyr--full-ui input[type=range]::-ms-thumb{background:var(--plyr-range-thumb-background, #fff);border:0;border-radius:100%;box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2));height:var(--plyr-range-thumb-height, 13px);position:relative;transition:all .2s ease;width:var(--plyr-range-thumb-height, 13px);margin-top:0}.plyr--full-ui input[type=range]::-ms-tooltip{display:none}.plyr--full-ui input[type=range]::-moz-focus-outer{border:0}.plyr--full-ui input[type=range]:focus{outline:0}.plyr--full-ui input[type=range]:focus-visible::-webkit-slider-runnable-track{outline:2px dashed var(--plyr-focus-visible-color, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));outline-offset:2px}.plyr--full-ui input[type=range]:focus-visible::-moz-range-track{outline:2px dashed var(--plyr-focus-visible-color, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));outline-offset:2px}.plyr--full-ui input[type=range]:focus-visible::-ms-track{outline:2px dashed var(--plyr-focus-visible-color, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));outline-offset:2px}.plyr__poster{background-color:var(--plyr-video-background, var(--plyr-video-background, rgb(0, 0, 0)));background-position:50% 50%;background-repeat:no-repeat;background-size:contain;height:100%;left:0;opacity:0;position:absolute;top:0;transition:opacity .2s ease;width:100%;z-index:1}.plyr--stopped.plyr__poster-enabled .plyr__poster{opacity:1}.plyr--youtube.plyr--paused.plyr__poster-enabled:not(.plyr--stopped) .plyr__poster{display:none}.plyr__time{font-size:var(--plyr-font-size-time, var(--plyr-font-size-small, 13px))}.plyr__time+.plyr__time:before{content:"\2044";margin-right:var(--plyr-control-spacing, 10px)}@media (max-width: 767px){.plyr__time+.plyr__time{display:none}}.plyr__tooltip{background:var(--plyr-tooltip-background, #fff);border-radius:var(--plyr-tooltip-radius, 5px);bottom:100%;box-shadow:var(--plyr-tooltip-shadow, 0 1px 2px rgba(0, 0, 0, .15));color:var(--plyr-tooltip-color, hsl(216, 15%, 34%));font-size:var(--plyr-font-size-small, 13px);font-weight:var(--plyr-font-weight-regular, 400);left:50%;line-height:1.3;margin-bottom:calc(calc(var(--plyr-control-spacing, 10px) / 2) * 2);opacity:0;padding:calc(var(--plyr-control-spacing, 10px) / 2) calc(calc(var(--plyr-control-spacing, 10px) / 2) * 1.5);pointer-events:none;position:absolute;transform:translate(-50%,10px) scale(.8);transform-origin:50% 100%;transition:transform .2s .1s ease,opacity .2s .1s ease;white-space:nowrap;z-index:2}.plyr__tooltip:before{border-left:var(--plyr-tooltip-arrow-size, 4px) solid transparent;border-right:var(--plyr-tooltip-arrow-size, 4px) solid transparent;border-top:var(--plyr-tooltip-arrow-size, 4px) solid var(--plyr-tooltip-background, #fff);bottom:calc(var(--plyr-tooltip-arrow-size, 4px) * -1);content:"";height:0;left:50%;position:absolute;transform:translate(-50%);width:0;z-index:2}.plyr .plyr__control:hover .plyr__tooltip,.plyr .plyr__control:focus-visible .plyr__tooltip,.plyr__tooltip--visible{opacity:1;transform:translate(-50%) scale(1)}.plyr .plyr__control:hover .plyr__tooltip{z-index:3}.plyr__controls>.plyr__control:first-child .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip{left:0;transform:translateY(10px) scale(.8);transform-origin:0 100%}.plyr__controls>.plyr__control:first-child .plyr__tooltip:before,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip:before{left:calc(var(--plyr-control-icon-size, 18px) / 2 + calc(var(--plyr-control-spacing, 10px) * .7))}.plyr__controls>.plyr__control:last-child .plyr__tooltip{left:auto;right:0;transform:translateY(10px) scale(.8);transform-origin:100% 100%}.plyr__controls>.plyr__control:last-child .plyr__tooltip:before{left:auto;right:calc(var(--plyr-control-icon-size, 18px) / 2 + calc(var(--plyr-control-spacing, 10px) * .7));transform:translate(50%)}.plyr__controls>.plyr__control:first-child:hover .plyr__tooltip,.plyr__controls>.plyr__control:first-child:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:first-child .plyr__tooltip--visible,.plyr__controls>.plyr__control:first-child+.plyr__control:hover .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:first-child+.plyr__control .plyr__tooltip--visible,.plyr__controls>.plyr__control:last-child:hover .plyr__tooltip,.plyr__controls>.plyr__control:last-child:focus-visible .plyr__tooltip,.plyr__controls>.plyr__control:last-child .plyr__tooltip--visible{transform:translate(0) scale(1)}.plyr__progress{left:calc(var(--plyr-range-thumb-height, 13px) * .5);margin-right:var(--plyr-range-thumb-height, 13px);position:relative}.plyr__progress input[type=range],.plyr__progress__buffer{margin-left:calc(var(--plyr-range-thumb-height, 13px) * -.5);margin-right:calc(var(--plyr-range-thumb-height, 13px) * -.5);width:calc(100% + var(--plyr-range-thumb-height, 13px))}.plyr__progress input[type=range]{position:relative;z-index:2}.plyr__progress .plyr__tooltip{left:0;max-width:120px;overflow-wrap:break-word}.plyr__progress__buffer{-webkit-appearance:none;background:transparent;border:0;border-radius:100px;height:var(--plyr-range-track-height, 5px);left:0;margin-top:calc(var(--plyr-range-track-height, 5px) / 2 * -1);padding:0;position:absolute;top:50%}.plyr__progress__buffer::-webkit-progress-bar{background:transparent}.plyr__progress__buffer::-webkit-progress-value{background:currentColor;border-radius:100px;min-width:var(--plyr-range-track-height, 5px);transition:width .2s ease}.plyr__progress__buffer::-moz-progress-bar{background:currentColor;border-radius:100px;min-width:var(--plyr-range-track-height, 5px);transition:width .2s ease}.plyr__progress__buffer::-ms-fill{border-radius:100px;transition:width .2s ease}.plyr--loading .plyr__progress__buffer{animation:plyr-progress 1s linear infinite;background-image:linear-gradient(-45deg,var(--plyr-progress-loading-background, rgba(35, 40, 47, .6)) 25%,transparent 25%,transparent 50%,var(--plyr-progress-loading-background, rgba(35, 40, 47, .6)) 50%,var(--plyr-progress-loading-background, rgba(35, 40, 47, .6)) 75%,transparent 75%,transparent);background-repeat:repeat-x;background-size:var(--plyr-progress-loading-size, 25px) var(--plyr-progress-loading-size, 25px);color:transparent}.plyr--video.plyr--loading .plyr__progress__buffer{background-color:var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, .25))}.plyr--audio.plyr--loading .plyr__progress__buffer{background-color:var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, .6))}.plyr__progress__marker{background-color:var(--plyr-progress-marker-background, #fff);border-radius:1px;height:var(--plyr-range-track-height, 5px);position:absolute;top:50%;transform:translate(-50%,-50%);width:var(--plyr-progress-marker-width, 3px);z-index:3}.plyr__volume{align-items:center;display:flex;position:relative}.plyr__volume input[type=range]{margin-left:calc(var(--plyr-control-spacing, 10px) / 2);margin-right:calc(var(--plyr-control-spacing, 10px) / 2);max-width:90px;min-width:60px;position:relative;z-index:2}.plyr--audio{display:block}.plyr--audio .plyr__controls{background:var(--plyr-audio-controls-background, #fff);border-radius:inherit;color:var(--plyr-audio-control-color, hsl(216, 15%, 34%));padding:var(--plyr-control-spacing, 10px)}.plyr--audio .plyr__control:focus-visible,.plyr--audio .plyr__control:hover,.plyr--audio .plyr__control[aria-expanded=true]{background:var(--plyr-audio-control-background-hover, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));color:var(--plyr-audio-control-color-hover, #fff)}.plyr--full-ui.plyr--audio input[type=range]::-webkit-slider-runnable-track{background-color:var(--plyr-audio-range-track-background, var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, .6)))}.plyr--full-ui.plyr--audio input[type=range]::-moz-range-track{background-color:var(--plyr-audio-range-track-background, var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, .6)))}.plyr--full-ui.plyr--audio input[type=range]::-ms-track{background-color:var(--plyr-audio-range-track-background, var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, .6)))}.plyr--full-ui.plyr--audio input[type=range]:active::-webkit-slider-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, .1))}.plyr--full-ui.plyr--audio input[type=range]:active::-moz-range-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, .1))}.plyr--full-ui.plyr--audio input[type=range]:active::-ms-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, .1))}.plyr--audio .plyr__progress__buffer{color:var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, .6))}.plyr--video{overflow:hidden}.plyr--video.plyr--menu-open{overflow:visible}.plyr__video-wrapper{background:var(--plyr-video-background, var(--plyr-video-background, rgb(0, 0, 0)));border-radius:inherit;height:100%;margin:auto;overflow:hidden;position:relative;width:100%}.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{aspect-ratio:16/9}@supports not (aspect-ratio: 16/9){.plyr__video-embed,.plyr__video-wrapper--fixed-ratio{height:0;padding-bottom:56.25%;position:relative}}.plyr__video-embed iframe,.plyr__video-wrapper--fixed-ratio video{border:0;height:100%;left:0;position:absolute;top:0;width:100%}.plyr--full-ui .plyr__video-embed>.plyr__video-embed__container{padding-bottom:240%;position:relative;transform:translateY(-38.28125%)}.plyr--video .plyr__controls{background:var(--plyr-video-controls-background, linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .75)));border-bottom-left-radius:inherit;border-bottom-right-radius:inherit;bottom:0;color:var(--plyr-video-control-color, #fff);left:0;padding:calc(var(--plyr-control-spacing, 10px) / 2);padding-top:calc(var(--plyr-control-spacing, 10px) * 2);position:absolute;right:0;transition:opacity .4s ease-in-out,transform .4s ease-in-out;z-index:3}@media (min-width: 480px){.plyr--video .plyr__controls{padding:var(--plyr-control-spacing, 10px);padding-top:calc(var(--plyr-control-spacing, 10px) * 3.5)}}.plyr--video.plyr--hide-controls .plyr__controls{opacity:0;pointer-events:none;transform:translateY(100%)}.plyr--video .plyr__control:focus-visible,.plyr--video .plyr__control:hover,.plyr--video .plyr__control[aria-expanded=true]{background:var(--plyr-video-control-background-hover, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));color:var(--plyr-video-control-color-hover, #fff)}.plyr__control--overlaid{background:var(--plyr-video-control-background-hover, var(--plyr-color-main, var(--plyr-color-main, hsl(198, 100%, 50%))));border:0;border-radius:100%;color:var(--plyr-video-control-color, #fff);display:none;left:50%;opacity:.9;padding:calc(var(--plyr-control-spacing, 10px) * 1.5);position:absolute;top:50%;transform:translate(-50%,-50%);transition:.3s;z-index:2}.plyr__control--overlaid svg{left:2px;position:relative}.plyr__control--overlaid:hover,.plyr__control--overlaid:focus{opacity:1}.plyr--playing .plyr__control--overlaid{opacity:0;visibility:hidden}.plyr--full-ui.plyr--video .plyr__control--overlaid{display:block}.plyr--full-ui.plyr--video input[type=range]::-webkit-slider-runnable-track{background-color:var(--plyr-video-range-track-background, var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, .25)))}.plyr--full-ui.plyr--video input[type=range]::-moz-range-track{background-color:var(--plyr-video-range-track-background, var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, .25)))}.plyr--full-ui.plyr--video input[type=range]::-ms-track{background-color:var(--plyr-video-range-track-background, var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, .25)))}.plyr--full-ui.plyr--video input[type=range]:active::-webkit-slider-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(255, 255, 255, .5))}.plyr--full-ui.plyr--video input[type=range]:active::-moz-range-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(255, 255, 255, .5))}.plyr--full-ui.plyr--video input[type=range]:active::-ms-thumb{box-shadow:var(--plyr-range-thumb-shadow, 0 1px 1px rgba(35, 40, 47, .15), 0 0 0 1px rgba(35, 40, 47, .2)),0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px) var(--plyr-audio-range-thumb-active-shadow-color, rgba(255, 255, 255, .5))}.plyr--video .plyr__progress__buffer{color:var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, .25))}.plyr:fullscreen{background:#000;border-radius:0!important;height:100%;margin:0;width:100%}.plyr:fullscreen video{height:100%}.plyr:fullscreen .plyr__control .icon--exit-fullscreen{display:block}.plyr:fullscreen .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr:fullscreen.plyr--hide-controls{cursor:none}@media (min-width: 1024px){.plyr:fullscreen .plyr__captions{font-size:var(--plyr-font-size-xlarge, 21px)}}.plyr--fullscreen-fallback{background:#000;border-radius:0!important;height:100%;margin:0;width:100%;bottom:0;left:0;position:fixed;right:0;top:0;z-index:10000000}.plyr--fullscreen-fallback video{height:100%}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen{display:block}.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen+svg{display:none}.plyr--fullscreen-fallback.plyr--hide-controls{cursor:none}@media (min-width: 1024px){.plyr--fullscreen-fallback .plyr__captions{font-size:var(--plyr-font-size-xlarge, 21px)}}.plyr__ads{border-radius:inherit;bottom:0;cursor:pointer;left:0;overflow:hidden;position:absolute;right:0;top:0;z-index:-1}.plyr__ads>div,.plyr__ads>div iframe{height:100%;position:absolute;width:100%}.plyr__ads:after{background:hsl(216,15%,16%);border-radius:2px;bottom:var(--plyr-control-spacing, 10px);color:#fff;content:attr(data-badge-text);font-size:11px;padding:2px 6px;pointer-events:none;position:absolute;right:var(--plyr-control-spacing, 10px);z-index:3}.plyr__ads:empty:after{display:none}.plyr__cues{background:currentColor;display:block;height:var(--plyr-range-track-height, 5px);left:0;opacity:.8;position:absolute;top:50%;transform:translateY(-50%);width:3px;z-index:3}.plyr__preview-thumb{background-color:var(--plyr-tooltip-background, #fff);border-radius:var(--plyr-menu-radius, 8px);bottom:100%;box-shadow:var(--plyr-tooltip-shadow, 0 1px 2px rgba(0, 0, 0, .15));margin-bottom:calc(calc(var(--plyr-control-spacing, 10px) / 2) * 2);opacity:0;padding:3px;pointer-events:none;position:absolute;transform:translateY(10px) scale(.8);transform-origin:50% 100%;transition:transform .2s .1s ease,opacity .2s .1s ease;z-index:2}.plyr__preview-thumb--is-shown{opacity:1;transform:translate(0) scale(1)}.plyr__preview-thumb:before{border-left:var(--plyr-tooltip-arrow-size, 4px) solid transparent;border-right:var(--plyr-tooltip-arrow-size, 4px) solid transparent;border-top:var(--plyr-tooltip-arrow-size, 4px) solid var(--plyr-tooltip-background, #fff);bottom:calc(var(--plyr-tooltip-arrow-size, 4px) * -1);content:"";height:0;left:calc(50% + var(--preview-arrow-offset));position:absolute;transform:translate(-50%);width:0;z-index:2}.plyr__preview-thumb__image-container{background:hsl(216,15%,79%);border-radius:calc(var(--plyr-menu-radius, 8px) - 1px);overflow:hidden;position:relative;z-index:0}.plyr__preview-thumb__image-container img,.plyr__preview-thumb__image-container:after{height:100%;left:0;position:absolute;top:0;width:100%}.plyr__preview-thumb__image-container:after{border-radius:inherit;box-shadow:inset 0 0 0 1px #00000026;content:"";pointer-events:none}.plyr__preview-thumb__image-container img{max-height:none;max-width:none}.plyr__preview-thumb__time-container{background:var(--plyr-video-controls-background, linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .75)));border-bottom-left-radius:calc(var(--plyr-menu-radius, 8px) - 1px);border-bottom-right-radius:calc(var(--plyr-menu-radius, 8px) - 1px);bottom:0;left:0;line-height:1.1;padding:20px 6px 6px;position:absolute;right:0;z-index:3}.plyr__preview-thumb__time-container span{color:#fff;font-size:var(--plyr-font-size-time, var(--plyr-font-size-small, 13px))}.plyr__preview-scrubbing{bottom:0;filter:blur(1px);height:100%;left:0;margin:auto;opacity:0;overflow:hidden;pointer-events:none;position:absolute;right:0;top:0;transition:opacity .3s ease;width:100%;z-index:1}.plyr__preview-scrubbing--is-shown{opacity:1}.plyr__preview-scrubbing img{height:100%;left:0;max-height:none;max-width:none;object-fit:contain;position:absolute;top:0;width:100%}.plyr--no-transition{transition:none!important}.plyr__sr-only{clip:rect(1px,1px,1px,1px);overflow:hidden;border:0!important;height:1px!important;padding:0!important;position:absolute!important;width:1px!important}.plyr [hidden]{display:none!important}.fm-modal{position:absolute;z-index:9998;top:0;bottom:0;left:0;right:0;width:100%;height:100%;background-color:#00000059;display:block;transition:opacity .4s ease;overflow:auto}.fm-modal .modal-xl{max-width:96%}.fm-modal-enter-active,.fm-modal-leave-active{transition:opacity .5s}.fm-modal-enter,.fm-modal-leave-to{opacity:0}.fm-info-block{flex:0 0 auto;padding-top:.2rem;padding-bottom:.4rem;border-bottom:1px solid #6c757d}.fm-info-block .progress{margin-top:.3rem}.fm-info-block .text-right>span{padding-left:.5rem;cursor:pointer}.fm-context-menu{position:absolute;z-index:9997;background-color:#fff;box-shadow:3px 2px 5px gray;border-radius:5px}.fm-context-menu:focus{outline:none}.fm-context-menu .list-unstyled{margin-bottom:0;border-bottom:1px solid rgba(0,0,0,.125)}.fm-context-menu ul>li{padding:.4rem 1rem}.fm-context-menu ul>li:not(.disabled){cursor:pointer}.fm-context-menu ul>li:not(.disabled):hover{background-color:#f8f9fa}.fm-context-menu ul>li:not(.disabled) i{padding-right:1.5rem}.fm-notification{position:absolute;right:1rem;bottom:0;z-index:9999;width:350px;display:block;transition:opacity .4s ease;overflow:auto}.fm-notification .fm-notification-item{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid;border-radius:.25rem}.fm-notification .notify-enter-active{transition:all .3s ease}.fm-notification .notify-leave-active{transition:all .8s ease}.fm-notification .notify-enter,.fm-notification .notify-leave-to{opacity:0}.fm{position:relative;height:100%;padding:1rem;background-color:#fff}.fm:-moz-full-screen{background-color:#fff}.fm:-webkit-full-screen{background-color:#fff}.fm:fullscreen{background-color:#fff}.fm .fm-body{flex:1 1 auto;overflow:hidden;position:relative;padding-top:1rem;padding-bottom:1rem;border-top:1px solid #6c757d;border-bottom:1px solid #6c757d}.fm .unselectable{user-select:none}.fm-error{color:#fff;background-color:#dc3545;border-color:#dc3545}.fm-danger{color:#dc3545;background-color:#fff;border-color:#dc3545}.fm-warning{color:#ffc107;background-color:#fff;border-color:#ffc107}.fm-success{color:#198754;background-color:#fff;border-color:#198754}.fm-info{color:#0dcaf0;background-color:#fff;border-color:#0dcaf0}.fm.fm-full-screen{width:100%;height:100%;padding-bottom:0} 10 | --------------------------------------------------------------------------------