├── src ├── Contracts │ ├── Attachable.php │ ├── PublicFile.php │ ├── OptimizesImages.php │ ├── Extensions.php │ ├── MimeTypes.php │ ├── ResizesImages.php │ └── CascadesFileDeletion.php ├── Http │ ├── Controllers │ │ ├── File │ │ │ ├── Share.php │ │ │ ├── Show.php │ │ │ ├── Download.php │ │ │ ├── Destroy.php │ │ │ ├── Link.php │ │ │ ├── MakePublic.php │ │ │ ├── MakePrivate.php │ │ │ ├── Favorite.php │ │ │ ├── Update.php │ │ │ ├── Recent.php │ │ │ ├── Browse.php │ │ │ └── Favorites.php │ │ ├── Type │ │ │ ├── Create.php │ │ │ ├── ExportExcel.php │ │ │ ├── InitTable.php │ │ │ ├── TableData.php │ │ │ ├── Edit.php │ │ │ ├── Destroy.php │ │ │ ├── Update.php │ │ │ └── Store.php │ │ └── Upload │ │ │ ├── Destroy.php │ │ │ └── Store.php │ ├── Requests │ │ ├── ValidateName.php │ │ ├── ValidateLink.php │ │ └── ValidateType.php │ └── Resources │ │ ├── Url.php │ │ ├── Type.php │ │ └── File.php ├── EnumServiceProvider.php ├── State │ └── Types.php ├── Enums │ └── TemporaryLinkDuration.php ├── AuthServiceProvider.php ├── Tables │ ├── Builders │ │ └── Type.php │ └── Templates │ │ └── types.json ├── Forms │ ├── Builders │ │ └── Type.php │ └── Templates │ │ └── type.json ├── Dynamics │ └── Relations │ │ └── FavoriteFiles.php ├── Policies │ ├── File.php │ └── Upload.php ├── AppServiceProvider.php ├── Models │ ├── Favorite.php │ ├── Upload.php │ ├── Type.php │ └── File.php ├── Services │ ├── ImageProcessor.php │ ├── Process.php │ ├── Validate.php │ └── Upload.php └── Exceptions │ └── File.php ├── .styleci.yml ├── routes ├── app │ ├── uploads.php │ ├── types.php │ └── files.php └── api.php ├── database ├── migrations │ ├── 2017_01_01_112500_create_structure_for_uploads.php │ ├── 2017_01_01_112400_create_uploads_table.php │ ├── 2017_01_01_112600_create_favorite_files_table.php │ ├── 2017_01_01_112100_create_file_types_table.php │ ├── 2017_01_01_112200_create_files_table.php │ ├── 2017_01_01_129000_create_structure_for_file_types.php │ └── 2017_01_01_112300_create_structure_for_files.php ├── factories │ └── TypeFactory.php └── seeders │ └── TypeSeeder.php ├── .github └── issue_template.md ├── codesize.xml ├── LICENSE ├── composer.json ├── config └── files.php ├── README.md └── tests └── features └── FileTest.php /src/Contracts/Attachable.php: -------------------------------------------------------------------------------- 1 | download(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Http/Controllers/Type/Create.php: -------------------------------------------------------------------------------- 1 | $form->create()]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Http/Controllers/Type/ExportExcel.php: -------------------------------------------------------------------------------- 1 | TemporaryLinkDuration::class, 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Http/Requests/ValidateName.php: -------------------------------------------------------------------------------- 1 | 'required|string|max:255']; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Http/Controllers/Type/Edit.php: -------------------------------------------------------------------------------- 1 | $form->edit($type)]; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /routes/app/uploads.php: -------------------------------------------------------------------------------- 1 | as('uploads.') 9 | ->group(function () { 10 | Route::post('store', Store::class)->name('store'); 11 | Route::delete('{upload}', Destroy::class)->name('destroy'); 12 | }); 13 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_112500_create_structure_for_uploads.php: -------------------------------------------------------------------------------- 1 | 'core.uploads.store', 'description' => 'Upload file', 'is_default' => true], 9 | ['name' => 'core.uploads.destroy', 'description' => 'Delete upload', 'is_default' => true], 10 | ]; 11 | }; 12 | -------------------------------------------------------------------------------- /src/Http/Controllers/File/Show.php: -------------------------------------------------------------------------------- 1 | authorize('access', $file); 16 | 17 | return $file->inline(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Controllers/File/Download.php: -------------------------------------------------------------------------------- 1 | authorize('access', $file); 16 | 17 | return $file->download(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Controllers/Upload/Destroy.php: -------------------------------------------------------------------------------- 1 | authorize('handle', $upload->file); 16 | 17 | $upload->delete(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/State/Types.php: -------------------------------------------------------------------------------- 1 | get()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Http/Resources/Url.php: -------------------------------------------------------------------------------- 1 | $this->id, 16 | 'path' => $this->path(), 17 | 'url' => "{$appUrl}/{$this->path()}", 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Http/Requests/ValidateLink.php: -------------------------------------------------------------------------------- 1 | 'required|in:'.TemporaryLinkDuration::keys()->implode(','), 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Enums/TemporaryLinkDuration.php: -------------------------------------------------------------------------------- 1 | '5m', 17 | self::OneHour => '1h', 18 | self::OneDay => '24h', 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Controllers/File/Destroy.php: -------------------------------------------------------------------------------- 1 | authorize('manage', $file); 17 | 18 | DB::transaction(fn () => $file->delete(true)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Http/Controllers/Upload/Store.php: -------------------------------------------------------------------------------- 1 | store($request->allFiles()); 15 | $files->each->loadData(); 16 | 17 | return File::collection($files); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Controllers/File/Link.php: -------------------------------------------------------------------------------- 1 | authorize('access', $file); 17 | 18 | return ['link' => $file->temporaryLink()]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Http/Controllers/File/MakePublic.php: -------------------------------------------------------------------------------- 1 | authorize('manage', $file); 17 | 18 | $file->update(['is_public' => true]); 19 | 20 | return new Resource($file); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/Controllers/Type/Destroy.php: -------------------------------------------------------------------------------- 1 | delete(); 16 | 17 | return [ 18 | 'message' => __('The file type was successfully deleted'), 19 | 'redirect' => 'administration.fileTypes.index', 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/Controllers/File/MakePrivate.php: -------------------------------------------------------------------------------- 1 | authorize('manage', $file); 17 | 18 | $file->update(['is_public' => false]); 19 | 20 | return new Resource($file); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/Controllers/File/Favorite.php: -------------------------------------------------------------------------------- 1 | authorize('access', $file); 18 | 19 | return ['isFavorite' => Model::toggle($request->user(), $file)]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Controllers/Type/Update.php: -------------------------------------------------------------------------------- 1 | fill($request->validated()); 14 | 15 | if ($type->isDirty('folder')) { 16 | $type->move(); 17 | } 18 | 19 | $type->save(); 20 | 21 | return ['message' => __('The file type was successfully updated')]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Http/Controllers/Type/Store.php: -------------------------------------------------------------------------------- 1 | fill($request->validated())->save(); 14 | 15 | return [ 16 | 'message' => __('The file type was created!'), 17 | 'redirect' => 'administration.fileTypes.edit', 18 | 'param' => ['type' => $type->id], 19 | ]; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Http/Controllers/File/Update.php: -------------------------------------------------------------------------------- 1 | authorize('manage', $file); 17 | 18 | $name = "{$request->get('name')}.{$file->extension()}"; 19 | 20 | $file->update(['original_name' => $name]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/AuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | UploadPolicy::class, 15 | File::class => FilePolicy::class, 16 | ]; 17 | 18 | public function boot() 19 | { 20 | $this->registerPolicies(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Tables/Builders/Type.php: -------------------------------------------------------------------------------- 1 | user()) 18 | ->between(json_decode($request->get('interval'), true)) 19 | ->filter($request->get('query')) 20 | ->get(); 21 | 22 | return Resource::collection($files); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Http/Resources/Type.php: -------------------------------------------------------------------------------- 1 | $this->id, 15 | 'name' => Str::title($this->name), 16 | 'icon' => $this->icon(), 17 | 'folder' => $this->folder, 18 | 'endpoint' => $this->endpoint, 19 | 'isBrowsable' => $this->is_browsable, 20 | 'isSystem' => $this->is_system, 21 | 'isUpload' => $this->model === Upload::class, 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Http/Controllers/File/Browse.php: -------------------------------------------------------------------------------- 1 | files() 18 | ->for($request->user()) 19 | ->between(json_decode($request->get('interval'), true)) 20 | ->filter($request->get('query')) 21 | ->get(); 22 | 23 | return File::collection($files); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | 2 | This is a **bug | feature request**. 3 | 4 | 5 | ### Prerequisites 6 | * [ ] Are you running the latest version? 7 | * [ ] Are you reporting to the correct repository? 8 | * [ ] Did you check the documentation? 9 | * [ ] Did you perform a cursory search? 10 | 11 | ### Description 12 | 13 | 14 | ### Steps to Reproduce 15 | 20 | 21 | ### Expected behavior 22 | 23 | 24 | ### Actual behavior 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Forms/Builders/Type.php: -------------------------------------------------------------------------------- 1 | form = (new Form($this->templatePath())); 17 | } 18 | 19 | public function create() 20 | { 21 | return $this->form->create(); 22 | } 23 | 24 | public function edit(Model $type) 25 | { 26 | return $this->form->edit($type); 27 | } 28 | 29 | protected function templatePath(): string 30 | { 31 | return self::TemplatePath; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | prefix('api/core') 8 | ->as('core.') 9 | ->group(function () { 10 | require __DIR__.'/app/files.php'; 11 | require __DIR__.'/app/uploads.php'; 12 | }); 13 | 14 | Route::middleware(['api', 'auth', 'core']) 15 | ->prefix('api/administration') 16 | ->as('administration.') 17 | ->group(function () { 18 | require __DIR__.'/app/types.php'; 19 | }); 20 | 21 | Route::middleware(['signed', 'bindings']) 22 | ->prefix('api/core/files') 23 | ->as('core.files.') 24 | ->group(function () { 25 | Route::get('share/{file}', Share::class)->name('share'); 26 | }); 27 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_112400_create_uploads_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 13 | 14 | $table->bigInteger('file_id')->unsigned()->nullable()->unique(); 15 | $table->foreign('file_id')->references('id')->on('files') 16 | ->onUpdate('restrict')->onDelete('restrict'); 17 | 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | public function down() 23 | { 24 | Schema::dropIfExists('uploads'); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/Http/Controllers/File/Favorites.php: -------------------------------------------------------------------------------- 1 | user() 17 | ->favoriteFiles() 18 | ->withData() 19 | ->between(json_decode($request->get('interval'), true)) 20 | ->filter($request->get('query')) 21 | ->paginated() 22 | ->latest('id') 23 | ->get(); 24 | 25 | return File::collection($files); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /codesize.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | custom rules 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Dynamics/Relations/FavoriteFiles.php: -------------------------------------------------------------------------------- 1 | $user->hasManyThrough( 26 | File::class, 27 | Favorite::class, 28 | 'user_id', 29 | 'id', 30 | 'id', 31 | 'file_id' 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Policies/File.php: -------------------------------------------------------------------------------- 1 | isSuperior()) { 16 | return true; 17 | } 18 | } 19 | 20 | public function access(User $user, Model $file) 21 | { 22 | return $file->is_public 23 | || $this->ownsFile($user, $file) 24 | || $file->type->isPublic(); 25 | } 26 | 27 | public function manage(User $user, Model $file) 28 | { 29 | return $this->ownsFile($user, $file); 30 | } 31 | 32 | protected function ownsFile(User $user, Model $file) 33 | { 34 | return $user->id === (int) $file->created_by; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Policies/Upload.php: -------------------------------------------------------------------------------- 1 | isSuperior()) { 16 | return true; 17 | } 18 | } 19 | 20 | public function view(User $user, Model $upload) 21 | { 22 | return $this->ownsUpload($user, $upload); 23 | } 24 | 25 | public function share(User $user, Model $upload) 26 | { 27 | return $this->ownsUpload($user, $upload); 28 | } 29 | 30 | public function destroy(User $user, Model $upload) 31 | { 32 | return $this->ownsUpload($user, $upload); 33 | } 34 | 35 | private function ownsUpload(User $user, Model $upload) 36 | { 37 | return $user->id === (int) $upload->created_by; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | load() 12 | ->publish(); 13 | } 14 | 15 | private function load() 16 | { 17 | $this->loadRoutesFrom(__DIR__.'/../routes/api.php'); 18 | 19 | $this->mergeConfigFrom(__DIR__.'/../config/files.php', 'enso.files'); 20 | 21 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 22 | 23 | return $this; 24 | } 25 | 26 | private function publish() 27 | { 28 | $this->publishes([ 29 | __DIR__.'/../config' => config_path('enso'), 30 | ], ['files-config', 'enso-config']); 31 | 32 | $this->publishes([ 33 | __DIR__.'/../database/factories' => database_path('factories'), 34 | ], ['files-factory', 'enso-factories']); 35 | 36 | return $this; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_112600_create_favorite_files_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->integer('user_id')->unsigned()->index(); 15 | $table->foreign('user_id')->references('id')->on('users'); 16 | 17 | $table->bigInteger('file_id')->unsigned()->index(); 18 | $table->foreign('file_id')->references('id')->on('files') 19 | ->onUpdate('restrict')->onDelete('restrict'); 20 | 21 | $table->timestamps(); 22 | 23 | $table->unique(['user_id', 'file_id']); 24 | $table->index(['created_at']); 25 | }); 26 | } 27 | 28 | public function down() 29 | { 30 | Schema::dropIfExists('favorite_files'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_112100_create_file_types_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->string('name')->unique(); 15 | $table->string('model')->unique()->nullable(); 16 | $table->string('folder')->nullable(); 17 | $table->string('icon')->nullable(); 18 | $table->string('endpoint')->nullable(); 19 | 20 | $table->text('description')->nullable(); 21 | 22 | $table->boolean('is_public'); 23 | $table->boolean('is_browsable'); 24 | $table->boolean('is_system'); 25 | 26 | $table->timestamps(); 27 | }); 28 | } 29 | 30 | public function down() 31 | { 32 | Schema::dropIfExists('file_types'); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 laravel-enso 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 | -------------------------------------------------------------------------------- /src/Models/Favorite.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 18 | } 19 | 20 | public function file() 21 | { 22 | return $this->belongsTo(File::class); 23 | } 24 | 25 | public static function toggle(User $user, File $file) 26 | { 27 | $isFavorite = ! static::for($user, $file)->first()?->delete(); 28 | 29 | if ($isFavorite) { 30 | self::create([ 31 | 'user_id' => $user->id, 32 | 'file_id' => $file->id, 33 | ]); 34 | } 35 | 36 | return $isFavorite; 37 | } 38 | 39 | public function scopeFor(Builder $query, User $user, File $file): Builder 40 | { 41 | return $query->whereUserId($user->id) 42 | ->whereFileId($file->id); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Http/Resources/File.php: -------------------------------------------------------------------------------- 1 | user()->can('access', $this->resource); 14 | 15 | return [ 16 | 'id' => $this->id, 17 | 'name' => $this->name(), 18 | 'extension' => $this->extension(), 19 | 'size' => DiskSize::forHumans($this->size), 20 | 'mimeType' => $this->mime_type, 21 | 'type' => new Type($this->whenLoaded('type')), 22 | 'owner' => new User($this->whenLoaded('createdBy')), 23 | 'isFavorite' => $this->relationLoaded('favorite') ? $this->favorite : false, 24 | 'isManageable' => $request->user()->can('manage', $this->resource), 25 | 'isAccessible' => $accessible, 26 | 'isPublic' => $this->is_public, 27 | 'createdAt' => $this->created_at->toDatetimeString(), 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /routes/app/types.php: -------------------------------------------------------------------------------- 1 | as('fileTypes.') 15 | ->group(function () { 16 | Route::get('create', Create::class)->name('create'); 17 | Route::post('', Store::class)->name('store'); 18 | Route::get('{type}/edit', Edit::class)->name('edit'); 19 | Route::patch('{type}', Update::class)->name('update'); 20 | Route::delete('{type}', Destroy::class)->name('destroy'); 21 | 22 | Route::get('initTable', InitTable::class)->name('initTable'); 23 | Route::get('tableData', TableData::class)->name('tableData'); 24 | Route::get('exportExcel', ExportExcel::class)->name('exportExcel'); 25 | }); 26 | -------------------------------------------------------------------------------- /database/factories/TypeFactory.php: -------------------------------------------------------------------------------- 1 | null, 17 | 'folder' => 'null', 18 | 'model' => null, 19 | 'icon' => 'folder', 20 | 'endpoint' => null, 21 | 'description' => null, 22 | 'is_public' => false, 23 | 'is_browsable' => false, 24 | 'is_system' => false, 25 | ]; 26 | } 27 | 28 | public function model(string $model): self 29 | { 30 | $name = Str::of($model)->afterLast('\\') 31 | ->snake() 32 | ->replace('_', ' ') 33 | ->title() 34 | ->plural(); 35 | 36 | return $this->state(fn () => [ 37 | 'name' => $name, 38 | 'folder' => $name->camel(), 39 | 'model' => $model, 40 | 'description' => "Enso {$name}", 41 | ]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_112200_create_files_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->bigInteger('type_id')->unsigned(); 15 | $table->foreign('type_id')->references('id')->on('file_types'); 16 | 17 | $table->nullableMorphs('attachable'); 18 | 19 | $table->string('original_name')->index(); 20 | $table->string('saved_name'); 21 | $table->integer('size'); 22 | $table->string('mime_type')->nullable(); 23 | 24 | $table->boolean('is_public'); 25 | 26 | $table->integer('created_by')->unsigned()->nullable(); 27 | $table->foreign('created_by')->references('id')->on('users'); 28 | 29 | $table->timestamps(); 30 | 31 | $table->index('created_at'); 32 | $table->index(['type_id', 'created_at']); 33 | }); 34 | } 35 | 36 | public function down() 37 | { 38 | Schema::dropIfExists('files'); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/Models/Upload.php: -------------------------------------------------------------------------------- 1 | belongsTo(File::class); 20 | } 21 | 22 | public static function cascadeFileDeletion(File $file): void 23 | { 24 | self::whereFileId($file->id)->first()->delete(); 25 | } 26 | 27 | public static function store(array $files): Collection 28 | { 29 | return DB::transaction(fn () => Collection::wrap($files) 30 | ->map(fn ($file) => self::upload($file))) 31 | ->values(); 32 | } 33 | 34 | protected static function upload(UploadedFile $file): File 35 | { 36 | $upload = self::create(); 37 | $file = File::upload($upload, $file); 38 | $upload->file()->associate($file)->save(); 39 | 40 | return $upload->file; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Services/ImageProcessor.php: -------------------------------------------------------------------------------- 1 | isImage()) { 23 | if (! empty($this->resize)) { 24 | $this->transformer() 25 | ->width($this->resize['width']) 26 | ->height($this->resize['height']); 27 | } 28 | 29 | if ($this->optimize) { 30 | $this->transformer()->optimize(); 31 | } 32 | } 33 | } 34 | 35 | private function isImage(): bool 36 | { 37 | return Validator::make( 38 | ['file' => $this->file], 39 | ['file' => 'image|mimetypes:'.implode(',', ImageTransformer::SupportedMimeTypes)] 40 | )->passes(); 41 | } 42 | 43 | private function transformer() 44 | { 45 | return $this->transformer ??= new ImageTransformer($this->file); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-enso/files", 3 | "description": "File manager dependency for Laravel Enso", 4 | "keywords": [ 5 | "laravel-enso", 6 | "files", 7 | "file-manager", 8 | "file-uploader" 9 | ], 10 | "homepage": "https://github.com/laravel-enso/files", 11 | "type": "library", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Adrian Ocneanu", 16 | "email": "aocneanu@gmail.com", 17 | "homepage": "https://laravel-enso.com", 18 | "role": "Developer" 19 | } 20 | ], 21 | "require": { 22 | "laravel-enso/core": "^10.0", 23 | "laravel-enso/helpers": "^3.0", 24 | "laravel-enso/image-transformer": "^2.0", 25 | "laravel-enso/migrator": "^2.0", 26 | "laravel-enso/track-who": "^2.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "LaravelEnso\\Files\\": "src/", 31 | "LaravelEnso\\Files\\Database\\Factories\\": "database/factories/", 32 | "LaravelEnso\\Files\\Database\\Seeders\\": "database/seeders/" 33 | } 34 | }, 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "LaravelEnso\\Files\\AppServiceProvider", 39 | "LaravelEnso\\Files\\AuthServiceProvider", 40 | "LaravelEnso\\Files\\EnumServiceProvider" 41 | ] 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /config/files.php: -------------------------------------------------------------------------------- 1 | (int) env('TEMPORARY_LINK_EXPIRATION', 60 * 60 * 24), 17 | 'storageLimit' => 500000, 18 | 'paginate' => (int) env('FILES_PAGINATION', 50), 19 | 'testingFolder' => 'testing', 20 | 'renameFolders' => [ 21 | 'dataImport' => 'import', 22 | 'dataExport' => 'export', 23 | 'webshopCarouselSlide' => 'carouselSlide', 24 | ], 25 | 'nonStandardFolders' => [ 26 | 'files', 'imports', 'carousel', 'howToVideos', 'webshopCarouselSlide', 27 | ], 28 | 'upgrade' => [ 29 | 'avatar' => Avatar::class, 30 | 'dataExport' => Export::class, 31 | 'upload' => Upload::class, 32 | 'dataImport' => Import::class, 33 | 'rejectedImport' => RejectedImport::class, 34 | 'document' => Document::class, 35 | 'productPicture' => Picture::class, 36 | 'webshopBrand' => Brand::class, 37 | 'webshopCarouselSlide' => CarouselSlide::class, 38 | 'poster' => Poster::class, 39 | 'video' => Video::class, 40 | ], 41 | ]; 42 | -------------------------------------------------------------------------------- /routes/app/files.php: -------------------------------------------------------------------------------- 1 | as('files.') 18 | ->group(function () { 19 | Route::get('link/{file}', Link::class)->name('link'); 20 | Route::get('download/{file}', Download::class)->name('download'); 21 | Route::delete('{file}', Destroy::class)->name('destroy'); 22 | Route::get('show/{file}', Show::class)->name('show'); 23 | Route::get('browse/{type}', Browse::class)->name('browse'); 24 | Route::get('recent', Recent::class)->name('recent'); 25 | Route::get('favorites', Favorites::class)->name('favorites'); 26 | Route::patch('{file}', Update::class)->name('update'); 27 | Route::patch('makePublic/{file}', MakePublic::class)->name('makePublic'); 28 | Route::patch('makePrivate/{file}', MakePrivate::class)->name('makePrivate'); 29 | Route::patch('favorite/{file}', Favorite::class)->name('favorite'); 30 | }); 31 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_129000_create_structure_for_file_types.php: -------------------------------------------------------------------------------- 1 | 'administration.fileTypes.tableData', 'description' => 'Get table data for file types', 'is_default' => false], 9 | ['name' => 'administration.fileTypes.exportExcel', 'description' => 'Export excel for file types', 'is_default' => false], 10 | ['name' => 'administration.fileTypes.initTable', 'description' => 'Init table data for file types', 'is_default' => false], 11 | ['name' => 'administration.fileTypes.create', 'description' => 'Create tutorial', 'is_default' => false], 12 | ['name' => 'administration.fileTypes.edit', 'description' => 'Edit tutorial', 'is_default' => false], 13 | ['name' => 'administration.fileTypes.index', 'description' => 'Show file types index', 'is_default' => false], 14 | ['name' => 'administration.fileTypes.store', 'description' => 'Store newly created file type', 'is_default' => false], 15 | ['name' => 'administration.fileTypes.update', 'description' => 'Update edited file type', 'is_default' => false], 16 | ['name' => 'administration.fileTypes.destroy', 'description' => 'Delete file type', 'is_default' => false], 17 | ]; 18 | 19 | protected array $menu = [ 20 | 'name' => 'File Types', 'icon' => 'photo-video', 'route' => 'administration.fileTypes.index', 'order_index' => 999, 'has_children' => false, 21 | ]; 22 | 23 | protected ?string $parentMenu = 'Administration'; 24 | }; 25 | -------------------------------------------------------------------------------- /database/migrations/2017_01_01_112300_create_structure_for_files.php: -------------------------------------------------------------------------------- 1 | 'core.files.index', 'description' => 'List files', 'is_default' => true], 9 | ['name' => 'core.files.link', 'description' => 'Get file download temporary link', 'is_default' => true], 10 | ['name' => 'core.files.show', 'description' => 'Open file in browser', 'is_default' => true], 11 | ['name' => 'core.files.download', 'description' => 'Download file', 'is_default' => true], 12 | ['name' => 'core.files.destroy', 'description' => 'Delete file', 'is_default' => true], 13 | ['name' => 'core.files.browse', 'description' => 'Browse file type', 'is_default' => true], 14 | ['name' => 'core.files.recent', 'description' => 'Browse recent files', 'is_default' => true], 15 | ['name' => 'core.files.favorites', 'description' => 'Browse favorites files', 'is_default' => true], 16 | ['name' => 'core.files.update', 'description' => 'Update file name', 'is_default' => true], 17 | ['name' => 'core.files.makePublic', 'description' => 'Make file public', 'is_default' => true], 18 | ['name' => 'core.files.makePrivate', 'description' => 'Make file private', 'is_default' => true], 19 | ['name' => 'core.files.favorite', 'description' => 'Toggle file as favorite', 'is_default' => true], 20 | ]; 21 | 22 | protected array $menu = [ 23 | 'name' => 'Files', 'icon' => 'folder-open', 'route' => 'core.files.index', 'order_index' => 255, 'has_children' => false, 24 | ]; 25 | }; 26 | -------------------------------------------------------------------------------- /src/Http/Requests/ValidateType.php: -------------------------------------------------------------------------------- 1 | ['required', 'string', $this->unique('name')], 21 | 'model' => ['nullable', 'required_if:is_system,true', 'string', $this->unique('model')], 22 | 'icon' => 'nullable|required_if:is_browsable,true|string', 23 | 'folder' => 'required_with:model|string', 24 | 'description' => 'nullable|string', 25 | 'is_public' => 'required|boolean', 26 | 'is_browsable' => 'required|boolean', 27 | 'is_system' => 'required|boolean', 28 | ]; 29 | } 30 | 31 | public function withValidator($validator) 32 | { 33 | $validator->after(fn ($validator) => $this->modelIsValid($validator)); 34 | } 35 | 36 | private function unique(string $attribute) 37 | { 38 | Rule::unique('file_types', $attribute) 39 | ->ignore($this->route('type')?->id); 40 | } 41 | 42 | private function modelIsValid($validator): void 43 | { 44 | $valid = class_exists($this->get('model')) 45 | && (new ReflectionClass($this->get('model'))) 46 | ->isSubclassOf(Model::class); 47 | 48 | if (! $valid) { 49 | $validator->errors()->add('model', __('Model is not valid')); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # File Manager 2 | 3 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/0ad3f6e21d9e42279dedd5432602891b)](https://www.codacy.com/gh/laravel-enso/files?utm_source=github.com&utm_medium=referral&utm_content=laravel-enso/files&utm_campaign=Badge_Grade) 4 | [![StyleCI](https://github.styleci.io/repos/85492361/shield?branch=master)](https://github.styleci.io/repos/85492361) 5 | [![License](https://poser.pugx.org/laravel-enso/datatable/license)](https://packagist.org/packages/laravel-enso/datatable) 6 | [![Total Downloads](https://poser.pugx.org/laravel-enso/files/downloads)](https://packagist.org/packages/laravel-enso/files) 7 | [![Latest Stable Version](https://poser.pugx.org/laravel-enso/files/version)](https://packagist.org/packages/laravel-enso/files) 8 | 9 | File manager dependency for [Laravel Enso](https://github.com/laravel-enso/Enso). 10 | 11 | This package works exclusively within the [Enso](https://github.com/laravel-enso/Enso) ecosystem. 12 | 13 | The front end assets that utilize this api are present in the [ui](https://github.com/enso-ui/ui) package. 14 | 15 | For live examples and demos, you may visit [laravel-enso.com](https://www.laravel-enso.com) 16 | 17 | [![Watch the demo](https://laravel-enso.github.io/files/screenshots/bulma_001_thumb.png)](https://laravel-enso.github.io/files/videos/bulma_filemanager.mp4) 18 | 19 | click on the photo to view a short demo in compatible browsers 20 | 21 | ### Installation, Configuration & Usage 22 | 23 | Be sure to check out the full documentation for this package available at [docs.laravel-enso.com](https://docs.laravel-enso.com/backend/files.html) 24 | 25 | ### Contributions 26 | 27 | are welcome. Pull requests are great, but issues are good too. 28 | 29 | ### License 30 | 31 | This package is released under the MIT license. 32 | -------------------------------------------------------------------------------- /src/Tables/Templates/types.json: -------------------------------------------------------------------------------- 1 | { 2 | "routePrefix": "administration.fileTypes", 3 | "crtNo": true, 4 | "buttons": [ 5 | "excel", 6 | "create", 7 | "edit", 8 | "destroy" 9 | ], 10 | "columns": [ 11 | { 12 | "label": "Name", 13 | "name": "name", 14 | "data": "file_types.name", 15 | "meta": [ 16 | "searchable", 17 | "sortable" 18 | ] 19 | }, 20 | { 21 | "label": "Icon", 22 | "name": "icon", 23 | "data": "file_types.icon", 24 | "meta": [ 25 | "icon", 26 | "method", 27 | "sortable" 28 | ] 29 | }, 30 | { 31 | "label": "Model", 32 | "name": "model", 33 | "data": "file_types.model", 34 | "meta": [ 35 | "searchable", 36 | "sortable" 37 | ] 38 | }, 39 | { 40 | "label": "Folder", 41 | "name": "folder", 42 | "data": "file_types.folder", 43 | "meta": [ 44 | "searchable", 45 | "sortable" 46 | ] 47 | }, 48 | { 49 | "label": "Public", 50 | "name": "is_public", 51 | "data": "file_types.is_public", 52 | "meta": [ 53 | "boolean", 54 | "sortable" 55 | ] 56 | }, 57 | { 58 | "label": "Browsable", 59 | "name": "is_browsable", 60 | "data": "file_types.is_browsable", 61 | "meta": [ 62 | "boolean", 63 | "sortable" 64 | ] 65 | }, 66 | { 67 | "label": "System", 68 | "name": "is_system", 69 | "data": "file_types.is_system", 70 | "meta": [ 71 | "boolean", 72 | "sortable" 73 | ] 74 | } 75 | ] 76 | } -------------------------------------------------------------------------------- /src/Services/Process.php: -------------------------------------------------------------------------------- 1 | width = null; 22 | $this->height = null; 23 | $this->optimize = false; 24 | } 25 | 26 | public function width(?int $width): self 27 | { 28 | $this->width = $width; 29 | 30 | return $this; 31 | } 32 | 33 | public function height(?int $height): self 34 | { 35 | $this->height = $height; 36 | 37 | return $this; 38 | } 39 | 40 | public function optimize(): self 41 | { 42 | $this->optimize = true; 43 | 44 | return $this; 45 | } 46 | 47 | public function handle(): void 48 | { 49 | $this->validate(); 50 | 51 | $transformer = new ImageTransformer($this->file); 52 | 53 | if ($this->width) { 54 | $transformer->width($this->width); 55 | } 56 | 57 | if ($this->height) { 58 | $transformer->height($this->height); 59 | } 60 | 61 | if ($this->optimize) { 62 | $transformer->optimize(); 63 | } 64 | } 65 | 66 | private function validate(): void 67 | { 68 | $validator = Validator::make( 69 | ['file' => $this->file], 70 | ['file' => 'image'] 71 | ); 72 | 73 | if ($validator->fails()) { 74 | throw Exception::invalidImage($this->file); 75 | } 76 | 77 | $mimeTypes = implode(',', ImageTransformer::SupportedMimeTypes); 78 | 79 | $validator = Validator::make( 80 | ['file' => $this->file], 81 | ['file' => 'mimetypes:'.$mimeTypes] 82 | ); 83 | 84 | if ($validator->fails()) { 85 | throw Exception::mimeType($this->file->getMimeType(), $mimeTypes); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Services/Validate.php: -------------------------------------------------------------------------------- 1 | extensions = null; 19 | $this->mimeTypes = null; 20 | } 21 | 22 | public function extensions(array $extensions) 23 | { 24 | $this->extensions = $extensions; 25 | } 26 | 27 | public function mimeTypes(array $mimeTypes) 28 | { 29 | $this->mimeTypes = $mimeTypes; 30 | } 31 | 32 | public function handle(): void 33 | { 34 | $this->file() 35 | ->extension() 36 | ->mimeType(); 37 | } 38 | 39 | private function file(): self 40 | { 41 | if ($this->file instanceof File) { 42 | if (! $this->file->isReadable()) { 43 | throw Exception::attach($this->file); 44 | } 45 | } elseif (! $this->file->isValid()) { 46 | throw Exception::upload($this->file); 47 | } 48 | 49 | return $this; 50 | } 51 | 52 | private function extension(): self 53 | { 54 | $valid = new Collection($this->extensions); 55 | $extension = $this->file instanceof UploadedFile 56 | ? $this->file->getClientOriginalExtension() 57 | : $this->file->extension(); 58 | $shouldThrow = $valid->isNotEmpty() && $valid->doesntContain($extension); 59 | 60 | if ($shouldThrow) { 61 | throw Exception::extension($extension, $valid->implode(',')); 62 | } 63 | 64 | return $this; 65 | } 66 | 67 | private function mimeType(): self 68 | { 69 | $valid = new Collection($this->mimeTypes); 70 | $mimeType = $this->file->getMimeType(); 71 | $shouldThrow = $valid->isNotEmpty() && $valid->doesntContain($mimeType); 72 | 73 | if ($shouldThrow) { 74 | throw Exception::mimeType($mimeType, $valid->implode(',')); 75 | } 76 | 77 | return $this; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Exceptions/File.php: -------------------------------------------------------------------------------- 1 | $files] 17 | )); 18 | } 19 | 20 | public static function attach(IlluminateFile $file) 21 | { 22 | return new static(__( 23 | 'Error attaching file :name', 24 | ['name' => $file->getBasename()] 25 | )); 26 | } 27 | 28 | public static function upload(UploadedFile $file) 29 | { 30 | return new static(__( 31 | 'Error uploading file :name', 32 | ['name' => $file->getClientOriginalName()] 33 | )); 34 | } 35 | 36 | public static function extension(string $extension, string $allowed) 37 | { 38 | return new static(__( 39 | 'Extension :extension is not allowed. Valid extensions are :allowed', 40 | ['extension' => $extension, 'allowed' => $allowed] 41 | )); 42 | } 43 | 44 | public static function mimeType(string $mime, string $allowed) 45 | { 46 | return new static(__( 47 | 'Mime type :mime not allowed. Allowed mime types are :allowed', 48 | ['mime' => $mime, 'allowed' => $allowed] 49 | )); 50 | } 51 | 52 | public static function invalidImage(SymfonyFile $file) 53 | { 54 | return new static(__( 55 | 'Invalid image :name', 56 | ['name' => $file->getBasename()] 57 | )); 58 | } 59 | 60 | //TODO remove 61 | public static function uploadError($file) 62 | { 63 | return new static(__( 64 | 'Error uploading file :name', 65 | ['name' => $file->getClientOriginalName()] 66 | )); 67 | } 68 | 69 | //TODO remove 70 | public static function invalidExtension($extension, $allowed) 71 | { 72 | return new static(__( 73 | 'Extension :extension is not allowed. Valid extensions are :allowed', 74 | ['extension' => $extension, 'allowed' => $allowed] 75 | )); 76 | } 77 | 78 | //TODO remove 79 | public static function invalidMimeType($mime, $allowed) 80 | { 81 | return new static(__( 82 | 'Mime type :mime not allowed. Allowed mime types are :allowed', 83 | ['mime' => $mime, 'allowed' => $allowed] 84 | )); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Forms/Templates/type.json: -------------------------------------------------------------------------------- 1 | { 2 | "routePrefix": "administration.fileTypes", 3 | "sections": [ 4 | { 5 | "columns": 2, 6 | "fields": [ 7 | { 8 | "label": "Name", 9 | "name": "name", 10 | "value": "", 11 | "meta": { 12 | "type": "input", 13 | "content": "text" 14 | } 15 | }, 16 | { 17 | "label": "Icon", 18 | "name": "icon", 19 | "value": "", 20 | "meta": { 21 | "type": "input", 22 | "content": "text" 23 | } 24 | }, 25 | { 26 | "label": "Model", 27 | "name": "model", 28 | "value": "", 29 | "meta": { 30 | "type": "input", 31 | "content": "text" 32 | } 33 | }, 34 | { 35 | "label": "Folder", 36 | "name": "folder", 37 | "value": "", 38 | "meta": { 39 | "type": "input", 40 | "content": "text" 41 | } 42 | } 43 | ] 44 | }, 45 | { 46 | "columns": 3, 47 | "fields": [ 48 | { 49 | "label": "Public", 50 | "name": "is_public", 51 | "value": false, 52 | "meta": { 53 | "type": "input", 54 | "content": "checkbox" 55 | } 56 | }, 57 | { 58 | "label": "Browsable", 59 | "name": "is_browsable", 60 | "value": false, 61 | "meta": { 62 | "type": "input", 63 | "content": "checkbox" 64 | } 65 | }, 66 | { 67 | "label": "System", 68 | "name": "is_system", 69 | "value": false, 70 | "meta": { 71 | "type": "input", 72 | "content": "checkbox" 73 | } 74 | } 75 | ] 76 | }, 77 | { 78 | "columns": 1, 79 | "fields": [ 80 | { 81 | "label": "Description", 82 | "name": "description", 83 | "value": "", 84 | "meta": { 85 | "type": "textarea" 86 | } 87 | } 88 | ] 89 | } 90 | ] 91 | } -------------------------------------------------------------------------------- /src/Models/Type.php: -------------------------------------------------------------------------------- 1 | hasMany(File::class); 30 | } 31 | 32 | public function scopeBrowsable(Builder $query): Builder 33 | { 34 | return $query->whereIsBrowsable(true); 35 | } 36 | 37 | public function scopeOrdered(Builder $query): Builder 38 | { 39 | return $query->orderByDesc('is_system')->orderBy('id'); 40 | } 41 | 42 | public function icon(): string|array 43 | { 44 | return Str::contains($this->icon, ' ') 45 | ? explode(' ', $this->icon) 46 | : $this->icon; 47 | } 48 | 49 | public function model(): Attachable 50 | { 51 | return new $this->model; 52 | } 53 | 54 | public function isPublic(): bool 55 | { 56 | return $this->is_public; 57 | } 58 | 59 | public static function for(string $model): self 60 | { 61 | return self::cacheGetBy('model', $model) 62 | ?? self::factory()->model($model)->create(); 63 | } 64 | 65 | public function folder(): string 66 | { 67 | $folder = App::runningUnitTests() 68 | ? Config::get('enso.files.testingFolder') 69 | : $this->folder; 70 | 71 | if (! Storage::has($folder)) { 72 | Storage::makeDirectory($folder); 73 | } 74 | 75 | return $folder; 76 | } 77 | 78 | public function path(string $filename): string 79 | { 80 | return "{$this->folder()}/{$filename}"; 81 | } 82 | 83 | public function move(): void 84 | { 85 | $from = Storage::path($this->getOriginal('folder')); 86 | $to = Storage::path($this->folder); 87 | 88 | if (FileFacade::isDirectory($to)) { 89 | FileFacade::copyDirectory($from, $to); 90 | FileFacade::deleteDirectory($from); 91 | } else { 92 | FileFacade::moveDirectory($from, $to); 93 | } 94 | } 95 | 96 | protected function casts(): array 97 | { 98 | return [ 99 | 'is_browsable' => 'boolean', 'is_system' => 'boolean', 100 | 'is_public' => 'boolean', 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Services/Upload.php: -------------------------------------------------------------------------------- 1 | type = Type::for($this->attachable::class); 28 | } 29 | 30 | public function handle(): File 31 | { 32 | return $this->validate() 33 | ->process() 34 | ->upload(); 35 | } 36 | 37 | private function validate(): self 38 | { 39 | $validator = new Validate($this->file); 40 | 41 | if ($this->attachable instanceof Extensions) { 42 | $validator->extensions($this->attachable->extensions()); 43 | } 44 | 45 | if ($this->attachable instanceof MimeTypes) { 46 | $validator->mimeTypes($this->attachable->mimeTypes()); 47 | } 48 | 49 | $validator->handle(); 50 | 51 | return $this; 52 | } 53 | 54 | private function process(): self 55 | { 56 | if (! $this->isSupportedImage()) { 57 | return $this; 58 | } 59 | 60 | if ($this->attachable instanceof ResizesImages) { 61 | $processor = (new Process($this->file)) 62 | ->width($this->attachable->imageWidth()) 63 | ->height($this->attachable->imageHeight()); 64 | } 65 | 66 | if ($this->attachable instanceof OptimizesImages) { 67 | $processor ??= new Process($this->file); 68 | $processor->optimize(); 69 | } 70 | 71 | if (isset($processor)) { 72 | $processor->handle(); 73 | } 74 | 75 | return $this; 76 | } 77 | 78 | private function upload(): File 79 | { 80 | $folder = $this->folder(); 81 | 82 | $model = File::create([ 83 | 'type_id' => $this->type->id, 84 | 'original_name' => $this->file->getClientOriginalName(), 85 | 'saved_name' => $this->file->hashName(), 86 | 'size' => $this->file->getSize(), 87 | 'mime_type' => $this->file->getMimeType(), 88 | 'is_public' => $this->type->isPublic(), 89 | ]); 90 | 91 | $this->file->store($folder); 92 | 93 | return $model; 94 | } 95 | 96 | private function folder() 97 | { 98 | $folder = App::runningUnitTests() 99 | ? Config::get('enso.files.testingFolder') 100 | : Type::for($this->attachable::class)->folder; 101 | 102 | if (! Storage::has($folder)) { 103 | Storage::makeDirectory($folder); 104 | } 105 | 106 | return $folder; 107 | } 108 | 109 | private function isSupportedImage(): bool 110 | { 111 | return Collection::wrap(ImageTransformer::SupportedMimeTypes) 112 | ->contains($this->file->getMimeType()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/features/FileTest.php: -------------------------------------------------------------------------------- 1 | seed() 30 | ->actingAs(User::first()); 31 | 32 | $this->file = UploadedFile::fake()->image('picture.png'); 33 | $this->createTestTable(); 34 | $this->model = AttachableModel::create(); 35 | } 36 | 37 | public function tearDown(): void 38 | { 39 | $this->cleanUp(); 40 | parent::tearDown(); 41 | } 42 | 43 | /** @test */ 44 | public function can_upload_file() 45 | { 46 | $file = File::upload($this->model, $this->file); 47 | $this->model->file()->associate($file)->save(); 48 | 49 | $this->assertNotNull($this->model->file); 50 | 51 | Storage::assertExists($this->model->file->path()); 52 | } 53 | 54 | /** @test */ 55 | public function can_attach_file() 56 | { 57 | $folder = Config::get('enso.files.testingFolder'); 58 | $filename = 'test.txt'; 59 | 60 | Storage::put("{$folder}/{$filename}", 'test'); 61 | 62 | $file = File::attach($this->model, $filename, $filename); 63 | 64 | $this->model->file()->associate($file)->save(); 65 | 66 | $this->assertNotNull($this->model->file); 67 | 68 | Storage::assertExists($this->model->file->path()); 69 | } 70 | 71 | /** @test */ 72 | public function cant_upload_file_with_invalid_extension() 73 | { 74 | $file = UploadedFile::fake()->image('image.jpg'); 75 | 76 | $this->expectException(Exception::class); 77 | 78 | $this->expectExceptionMessage( 79 | Exception::invalidExtension($file->getClientOriginalExtension(), 'png') 80 | ->getMessage() 81 | ); 82 | 83 | File::upload($this->model, $file); 84 | } 85 | 86 | /** @test */ 87 | public function cant_upload_file_with_invalid_mime_type() 88 | { 89 | $file = UploadedFile::fake()->create('doc.doc', 0, 'application/msword'); 90 | 91 | $this->expectException(Exception::class); 92 | 93 | $this->expectExceptionMessage( 94 | Exception::invalidMimeType($file->getMimeType(), 'image/png') 95 | ->getMessage() 96 | ); 97 | 98 | $this->model->extension = 'doc'; 99 | File::upload($this->model, $file); 100 | } 101 | 102 | /** @test */ 103 | public function can_display_file_inline() 104 | { 105 | $file = File::upload($this->model, $this->file); 106 | $this->model->file()->associate($file)->save(); 107 | 108 | $response = $this->model->file->inline($this->file->hashname()); 109 | 110 | $this->assertEquals(200, $response->getStatusCode()); 111 | } 112 | 113 | /** @test */ 114 | public function can_download_file() 115 | { 116 | $file = File::upload($this->model, $this->file); 117 | $this->model->file()->associate($file)->save(); 118 | 119 | $response = $this->model->file->download(); 120 | 121 | $this->assertEquals(200, $response->getStatusCode()); 122 | } 123 | 124 | private function createTestTable(): self 125 | { 126 | Schema::create('attachable_models', function ($table) { 127 | $table->increments('id'); 128 | 129 | $table->bigInteger('file_id')->unsigned()->nullable(); 130 | $table->foreign('file_id')->references('id')->on('files') 131 | ->onUpdate('restrict')->onDelete('restrict'); 132 | 133 | $table->timestamps(); 134 | }); 135 | 136 | return $this; 137 | } 138 | 139 | private function cleanUp() 140 | { 141 | Storage::deleteDirectory(Config::get('enso.files.testingFolder')); 142 | } 143 | } 144 | 145 | class AttachableModel extends Model implements Attachable, Extensions, MimeTypes 146 | { 147 | public string $extension = 'png'; 148 | 149 | public function file(): Relation 150 | { 151 | return $this->belongsTo(File::class); 152 | } 153 | 154 | public function extensions(): array 155 | { 156 | return [$this->extension]; 157 | } 158 | 159 | public function mimeTypes(): array 160 | { 161 | return ['image/png']; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /database/seeders/TypeSeeder.php: -------------------------------------------------------------------------------- 1 | avatars() 24 | ->recents() 25 | ->favorites() 26 | ->uploads() 27 | ->exports() 28 | ->imports() 29 | ->rejectedImports() 30 | ->documents() 31 | ->productPictures() 32 | ->brands() 33 | ->carouselSlides() 34 | ->howToPosters() 35 | ->howToVideos(); 36 | } 37 | 38 | private function avatars(): self 39 | { 40 | Type::factory()->model(Avatar::class)->create(); 41 | 42 | return $this; 43 | } 44 | 45 | private function recents(): self 46 | { 47 | Type::factory()->create([ 48 | 'name' => 'Recents', 49 | 'folder' => null, 50 | 'model' => null, 51 | 'icon' => 'folder-plus', 52 | 'endpoint' => 'recent', 53 | 'description' => 'Recent files', 54 | 'is_browsable' => true, 55 | 'is_system' => true, 56 | ]); 57 | 58 | return $this; 59 | } 60 | 61 | private function favorites(): self 62 | { 63 | Type::factory()->create([ 64 | 'name' => 'Favorites', 65 | 'folder' => null, 66 | 'model' => null, 67 | 'icon' => 'star', 68 | 'endpoint' => 'favorites', 69 | 'description' => 'User Favorites', 70 | 'is_browsable' => true, 71 | 'is_system' => true, 72 | ]); 73 | 74 | return $this; 75 | } 76 | 77 | private function uploads(): self 78 | { 79 | Type::factory()->model(Upload::class)->create([ 80 | 'name' => 'Uploads', 81 | 'icon' => 'file-upload', 82 | 'is_browsable' => true, 83 | 'is_system' => false, 84 | ]); 85 | 86 | return $this; 87 | } 88 | 89 | private function exports(): self 90 | { 91 | Type::factory()->model(Export::class)->create([ 92 | 'icon' => 'file-export', 93 | 'is_browsable' => true, 94 | 'is_system' => false, 95 | ]); 96 | 97 | return $this; 98 | } 99 | 100 | private function imports(): self 101 | { 102 | if (class_exists(Import::class)) { 103 | Type::factory()->model(Import::class)->create([ 104 | 'icon' => 'file-import', 105 | 'is_browsable' => true, 106 | ]); 107 | } 108 | 109 | return $this; 110 | } 111 | 112 | private function rejectedImports(): self 113 | { 114 | if (class_exists(RejectedImport::class)) { 115 | Type::factory()->model(RejectedImport::class)->create([ 116 | 'icon' => 'exclamation-triangle', 117 | 'is_browsable' => true, 118 | ]); 119 | } 120 | 121 | return $this; 122 | } 123 | 124 | private function documents(): self 125 | { 126 | if (class_exists(Document::class)) { 127 | Type::factory()->model(Document::class)->create([ 128 | 'icon' => 'file-contract', 129 | 'is_browsable' => true, 130 | ]); 131 | } 132 | 133 | return $this; 134 | } 135 | 136 | private function productPictures(): self 137 | { 138 | if (class_exists(Picture::class)) { 139 | Type::factory()->model(Picture::class)->create([ 140 | 'icon' => 'image', 141 | 'is_browsable' => true, 142 | ]); 143 | } 144 | 145 | return $this; 146 | } 147 | 148 | private function brands(): self 149 | { 150 | if (class_exists(Brand::class)) { 151 | Type::factory()->model(Brand::class)->create([ 152 | 'is_browsable' => true, 153 | 'icon' => 'copyright', 154 | ]); 155 | } 156 | 157 | return $this; 158 | } 159 | 160 | private function carouselSlides(): self 161 | { 162 | if (class_exists(CarouselSlide::class)) { 163 | Type::factory()->model(CarouselSlide::class)->create(); 164 | } 165 | 166 | return $this; 167 | } 168 | 169 | private function howToPosters(): self 170 | { 171 | if (class_exists(Poster::class)) { 172 | Type::factory()->model(Poster::class)->create(); 173 | } 174 | 175 | return $this; 176 | } 177 | 178 | private function howToVideos(): self 179 | { 180 | if (class_exists(Video::class)) { 181 | Type::factory()->model(Video::class)->create(); 182 | } 183 | 184 | return $this; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Models/File.php: -------------------------------------------------------------------------------- 1 | belongsTo(Type::class); 36 | } 37 | 38 | public function favorites() 39 | { 40 | return $this->hasMany(Favorite::class); 41 | } 42 | 43 | public function favorite() 44 | { 45 | return $this->hasOne(Favorite::class) 46 | ->whereUserId(Auth::id()); 47 | } 48 | 49 | public function temporaryLink(?int $minutes = null): string 50 | { 51 | $limit = $minutes ?? Config::get('enso.files.linkExpiration'); 52 | $expires = Carbon::now()->addSeconds($limit); 53 | $args = ['core.files.share', $expires, ['file' => $this->id]]; 54 | 55 | return URL::temporarySignedRoute(...$args); 56 | } 57 | 58 | public function scopeBrowsable(Builder $query): Builder 59 | { 60 | return $query->whereHas('type', fn ($type) => $type->browsable()); 61 | } 62 | 63 | public function scopeWithData(Builder $query): Builder 64 | { 65 | $attrs = ['type', 'createdBy.person', 'createdBy.avatar', 'favorite']; 66 | 67 | return $query->with($attrs); 68 | } 69 | 70 | public function scopePublic(Builder $query): Builder 71 | { 72 | return $query->whereIsPublic(true); 73 | } 74 | 75 | public function scopeFor(Builder $query, User $user): Builder 76 | { 77 | return $query->browsable() 78 | ->withData() 79 | ->when(! $user->isSuperior(), fn ($query) => $query 80 | ->whereCreatedBy($user->id)->orWhere->public()) 81 | ->latest('id') 82 | ->paginated(); 83 | } 84 | 85 | public function scopePaginated(Builder $query): Builder 86 | { 87 | return $query->limit(Config::get('enso.files.paginate')); 88 | } 89 | 90 | public function scopeBetween(Builder $query, array $interval): Builder 91 | { 92 | return $query 93 | ->when($interval['min'], fn ($query) => $query 94 | ->where('files.created_at', '>=', Carbon::parse($interval['min']))) 95 | ->when($interval['max'], fn ($query) => $query 96 | ->where('files.created_at', '<=', Carbon::parse($interval['max']))); 97 | } 98 | 99 | public function scopeFilter(Builder $query, ?string $search): Builder 100 | { 101 | return $query->when($search, fn ($query) => $query 102 | ->where('original_name', 'LIKE', '%'.$search.'%')); 103 | } 104 | 105 | public function loadData(): self 106 | { 107 | $attrs = ['type', 'createdBy.person', 'createdBy.avatar', 'favorite']; 108 | 109 | return $this->load($attrs); 110 | } 111 | 112 | public function asciiName(): string 113 | { 114 | return Str::ascii($this->original_name); 115 | } 116 | 117 | public function name(): string 118 | { 119 | return Str::beforeLast($this->asciiName(), '.'); 120 | } 121 | 122 | public function extension(): string 123 | { 124 | return Str::afterLast($this->asciiName(), '.'); 125 | } 126 | 127 | public function path(): string 128 | { 129 | return "{$this->type->folder()}/{$this->saved_name}"; 130 | } 131 | 132 | public static function attach(Attachable $attachable, string $savedName, string $filename, ?int $userId = null): self 133 | { 134 | $type = Type::for($attachable::class); 135 | $file = new IlluminateFile(Storage::path($type->path($savedName))); 136 | 137 | return self::create([ 138 | 'type_id' => $type->id, 139 | 'original_name' => $filename, 140 | 'saved_name' => $savedName, 141 | 'size' => $file->getSize(), 142 | 'mime_type' => $file->getMimeType(), 143 | 'is_public' => $type->isPublic(), 144 | 'created_by' => $userId, 145 | ]); 146 | } 147 | 148 | public static function upload(Attachable $attachable, UploadedFile $file): self 149 | { 150 | return (new Upload($attachable, $file))->handle(); 151 | } 152 | 153 | public function delete(bool $cascadable = false) 154 | { 155 | $cascadesDeletion = $cascadable 156 | && (new ReflectionClass($this->type->model)) 157 | ->implementsInterface(CascadesFileDeletion::class); 158 | 159 | if ($cascadesDeletion) { 160 | $this->type->model::cascadeFileDeletion($this); 161 | } 162 | 163 | $this->favorites->each->delete(); 164 | 165 | Storage::delete($this->path()); 166 | 167 | return parent::delete(); 168 | } 169 | 170 | public function download(): StreamedResponse 171 | { 172 | return Storage::download($this->path(), $this->asciiName()); 173 | } 174 | 175 | public function inline(): StreamedResponse 176 | { 177 | return Storage::response($this->path()); 178 | } 179 | 180 | public function processImage(BaseFile $file): void 181 | { 182 | $optimizeImages = $this->attachable->optimizeImages(); 183 | $resizeImages = $this->attachable->resizeImages(); 184 | 185 | (new ImageProcessor($file, $optimizeImages, $resizeImages))->handle(); 186 | } 187 | 188 | public function isImage(BaseFile $file): bool 189 | { 190 | $mimeTypes = implode(',', ImageTransformer::SupportedMimeTypes); 191 | 192 | return Validator::make( 193 | ['file' => $file], 194 | ['file' => "image|mimetypes:{$mimeTypes}"] 195 | )->passes(); 196 | } 197 | 198 | protected function casts(): array 199 | { 200 | return [ 201 | 'is_public' => 'boolean', 202 | ]; 203 | } 204 | } 205 | --------------------------------------------------------------------------------