├── 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 | [](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 | [](https://github.styleci.io/repos/85492361)
5 | [](https://packagist.org/packages/laravel-enso/datatable)
6 | [](https://packagist.org/packages/laravel-enso/files)
7 | [](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 | [](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 |
--------------------------------------------------------------------------------