diff --git a/app/Http/Controllers/FileController.php b/app/Http/Controllers/FileController.php index 4a5e417..ad737a1 100644 --- a/app/Http/Controllers/FileController.php +++ b/app/Http/Controllers/FileController.php @@ -17,9 +17,18 @@ use function PHPUnit\Framework\isNull; class FileController extends Controller { - public function show($collection, $uuid, $ext) + public function show($collection, $uuid, $ext, Request $request, ImageProcessor $imageProcessor) { - dump($collection, $uuid, $ext); + if ($request->castParams == 'resource') { + return new FileResource(app()->file); + } + + if (!is_null(array_intersect(array_keys($request->all()), $this->availableParams))) { + $imageProcessor->process(app()->file->getPath(), app()->file->getModifiedPath(), $request->all()); + return response()->file(app()->file->getModifiedPath()); + } + + return response()->file(app()->file->getPath()); } public function private($collection, $path) @@ -29,14 +38,16 @@ class FileController extends Controller public function store(FileStoreRequest $request, ImageProcessor $imageProcessor) { - $request->file = $imageProcessor->convertImage($request->file->path(),'/tmp/' . app()->uuid . '.' . app()->collection->ext); + $request->file = $imageProcessor->convertImage($request->file->path(), '/tmp/' . app()->uuid . '.' . app()->collection->ext); if (!is_null(app()->collection->process)) { - $request->file = $imageProcessor->process($request->file->path(),'/tmp/' . app()->uuid. '.' . app()->collection->ext, app()->collection->process); + $request->file = $imageProcessor->process($request->file->path(), '/tmp/' . app()->uuid . '.' . app()->collection->ext, app()->collection->process); } - DB::transaction(function () use ($request) { + + $fileResource = null; + DB::transaction(function () use ($request,&$fileResource) { $uuid = app()->uuid; - $request->resourceFile = File::create([ + $fileResource = File::create([ 'uuid' => $uuid, 'original_name' => $request->name, 'public' => $request->public, @@ -58,12 +69,12 @@ class FileController extends Controller File::where('user_id', auth()->id())->delete(); } - $storedFile = Storage::disk(app()->collection->disk)->putFileAs('', $request->file,$request->resourceFile->server_path . $request->resourceFile->uuid . '.' . app()->collection->ext); + $storedFile = Storage::disk(app()->collection->disk)->putFileAs($fileResource->server_path, $request->file, $fileResource->uuid . '.' . app()->collection->ext); if (app()->collection->public) { Storage::disk(app()->collection->disk)->setVisibility($storedFile, 'public'); } }); - return new FileResource($request->resourceFile); + return new FileResource($fileResource); } public function update(Request $request, $id) diff --git a/app/Http/Controllers/Traits/FileTrait.php b/app/Http/Controllers/Traits/FileTrait.php index a413431..0e22e27 100644 --- a/app/Http/Controllers/Traits/FileTrait.php +++ b/app/Http/Controllers/Traits/FileTrait.php @@ -4,5 +4,5 @@ namespace App\Http\Controllers\Traits; trait FileTrait { - + public $availableParams = ['r','w','h','canv','brightness','saturation','hue','rotation','flip']; } diff --git a/app/Http/Middleware/BindCollectionModelMiddleware.php b/app/Http/Middleware/BindCollectionModelMiddleware.php index f984c8c..460a9fb 100644 --- a/app/Http/Middleware/BindCollectionModelMiddleware.php +++ b/app/Http/Middleware/BindCollectionModelMiddleware.php @@ -18,7 +18,7 @@ class BindCollectionModelMiddleware */ public function handle(Request $request, Closure $next) { - if ($request->route()->action['as'] == 'api.files.store') { + if (($request->route()->action['as'] == 'api.files.store') || ($request->route()->action['as'] == "api.files.show")) { app()->singleton('collection', function () use ($request) { return Collection::where('name', $request->route('collection_name'))->firstOrFail(); }); diff --git a/app/Http/Middleware/BindFileModelMiddleware.php b/app/Http/Middleware/BindFileModelMiddleware.php index 8bfad45..854a13b 100644 --- a/app/Http/Middleware/BindFileModelMiddleware.php +++ b/app/Http/Middleware/BindFileModelMiddleware.php @@ -32,6 +32,18 @@ class BindFileModelMiddleware } } + if ($request->route()->action['as'] == 'api.files.show') { + $file = File::findOrFail($request->route('uuid')); + $Collection = Collection::where('name', $request->route('collection_name'))->firstOrFail(); + if (Storage::disk($Collection->disk)->exists($file->server_path . $file->uuid . '.' . $Collection->ext)) { + app()->bind('file', function () use ($file) { + return $file; + }); + }else{ + abort(404); + } + } + return $next($request); } } diff --git a/app/Http/Requests/FileStoreRequest.php b/app/Http/Requests/FileStoreRequest.php index 45b602d..8fdb029 100644 --- a/app/Http/Requests/FileStoreRequest.php +++ b/app/Http/Requests/FileStoreRequest.php @@ -30,8 +30,7 @@ class FileStoreRequest extends FormRequest return false; } - if (app()->collection->count !== 1 && (app()->collection->count <= File::where('model_id', auth()->id())->count()) && !app()->collection->tmp_support) { - + if (app()->collection->count !== 1 && (app()->collection->count <= File::where('user_id', auth()->id())->where('collction_id',app()->collection->id)->count()) && !app()->collection->tmp_support) { return false; } if (!app()->bound('file') && is_null($this->file('file'))) { @@ -40,7 +39,7 @@ class FileStoreRequest extends FormRequest if (!$this->hasFile('file')) { $this->replace([ - 'file' => new \Illuminate\Http\File(Storage::disk(app()->collection->disk)->path(app()->file->server_path . app()->file->uuid . '.' . app()->collection->ext), app()->file->uuid . '.' . app()->collection->ext) + 'file' => new \Illuminate\Http\File(app()->file->getPath(), app()->file->uuid . '.' . app()->collection->ext) ]); } diff --git a/app/Image/ImageProcessor.php b/app/Image/ImageProcessor.php index 954655c..ca70eab 100644 --- a/app/Image/ImageProcessor.php +++ b/app/Image/ImageProcessor.php @@ -2,71 +2,121 @@ namespace App\Image; +use App\Http\Controllers\Traits\FileTrait; +use App\Image\Traits\ModulateTrait; +use Illuminate\Contracts\Validation\Rule; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rule as ValidationRule; use Jcupitt\Vips\Image; use Jcupitt\Vips\Interpretation; class ImageProcessor { - public function process(string $filename, string $target, array $options = ["w" => null, "h" => null, "r" => null,"flip" => null , "canv" => null,"rotation" => null],$saveOptions = ['Q' => 100]) + use ModulateTrait, FileTrait; + + protected $defaultW = 2000; + protected $defaultH = 2000; + + public function process(string $filename, string $target, array $options = [], $saveOptions = ['Q' => 100]) { - if (!isset($options['w']) && !isset($options['h'])) { - $image = Image::thumbnail($filename, getimagesize($filename)[1]); + + + if (array_key_exists('w', $options) && array_key_exists('h', $options) && array_key_exists('r', $options)) { + unset($options['r']); } - if (isset($options['r'])) { - $rArray = explode(':', $options['r']); + if (array_key_exists('r', $options)) { + $options['r'] = explode(':', $options['r']); + if (count($options['r']) != 2) { + unset($options['r']); + } - if (isset($options['w']) && !isset($options['h'])) { - $options['h'] = $options['w'] * $rArray[1]; - $options['w'] = $options['w'] * $rArray[0]; + $validator = Validator::make($options, [ + 'r' => ['nullable', 'array'], + 'r.*' => ['nullable', 'numeric'], + ]); + if ($validator->fails()) { + foreach ($validator->messages()->getMessages() as $field_name => $messages) { + unset($options[explode('.', $field_name)[0]]); + } } + } + + if (array_key_exists('r', $options)) { + if (array_key_exists('w', $options) && !array_key_exists('h', $options)) { + $options['w'] = $options['w'] * $options['r'][0]; + $options['h'] = $options['w'] * $options['r'][1]; + } - if (!isset($options['w']) && isset($options['h'])) { - $options['h'] = $options['h'] * $rArray[1]; - $options['w'] = $options['h'] * $rArray[0]; + if (!array_key_exists('w', $options) && array_key_exists('h', $options)) { + $options['w'] = $options['h'] * $options['r'][0]; + $options['h'] = $options['h'] * $options['r'][1]; } - if (!isset($options['w']) && !isset($options['h'])) { - $options['h'] = getimagesize($filename)[0] * $rArray[1]; - $options['w'] = getimagesize($filename)[0] * $rArray[0]; + if (!array_key_exists('w', $options) && !array_key_exists('h', $options)) { + $options['w'] = getimagesize($filename)[0] * $options['r'][0]; + $options['h'] = getimagesize($filename)[0] * $options['r'][1]; } + + unset($options['r']); } - if (isset($options['w']) && !isset($options['h'])) { - $image = Image::thumbnail($filename, $options['w'], ['height' => getimagesize($filename)[1]]); + $validator = Validator::make($options, [ + 'w' => ['numeric', 'between:1,2000'], + 'h' => ['numeric', 'between:1,2000'], + 'canv' => ['boolean'], + 'brightness' => ['numeric', 'between:0,100'], + 'saturation' => ['numeric', 'between:0,100'], + 'hue' => ['numeric', 'between:0,100'], + 'rotation' => ['numeric'], + 'flip' => ['string', ValidationRule::in(['h', 'v', 'hv'])] + ]); + + if ($validator->fails()) { + foreach ($validator->messages()->getMessages() as $field_name => $messages) { + unset($options[$field_name]); + } } - if (!isset($options['w']) && isset($options['h'])) { - $image = Image::thumbnail($filename, getimagesize($filename)[0], ['height' => $options['h']]); + + if (!array_key_exists('w', $options) && !array_key_exists('h', $options)) { + $image = Image::newFromFile($filename); } + if (array_key_exists('w', $options) && !array_key_exists('h', $options)) { + $image = Image::thumbnail($filename, $options['w'], ['height' => $this->defaultH]); + } - if (isset($options['w']) && isset($options['h']) && !($options['canv'] == true)) { - $image = Image::thumbnail($filename, $options['w'], ['height' => $options['h'], 'crop' => 'centre']); + if (!array_key_exists('w', $options) && array_key_exists('h', $options)) { + $image = Image::thumbnail($filename, $this->defaultW, ['height' => $options['h']]); } - if (isset($options['w']) && isset($options['h']) && $options['canv'] == true) { - $image = Image::thumbnail($filename, $options['w'], ['height' => $options['h']]); - $widthH = ($options['h'] - $image->height) / 2; - $widthW = ($options['w'] - $image->width) / 2; - $image = $image->embed( - $widthW, - $widthH, - $options['w'], - $options['h'], - ['extend' => 'background', 'background' => 1024] - ); + if (array_key_exists('w', $options) && array_key_exists('h', $options)) { + if (array_key_exists('canv', $options) && ($options['canv'] == true)) { + $image = Image::thumbnail($filename, $options['w'], ['height' => $options['h']]); + $widthH = ($options['h'] - $image->height) / 2; + $widthW = ($options['w'] - $image->width) / 2; + $image = $image->embed( + $widthW, + $widthH, + $options['w'], + $options['h'], + ['extend' => 'background', 'background' => 1024] + ); + } else { + $image = Image::thumbnail($filename, $options['w'], ['height' => $options['h'], 'crop' => 'centre']); + } } - if (isset($options['brightness']) || isset($options['saturation']) || isset($options['hue'])) { - $image = $this->brightness($image, isset($options['brightness']) ? $options['brightness'] : 1.0, isset($options['saturation']) ? $options['saturation'] : 1.0, isset($options['hue']) ? $options['hue'] : 0.0); + if (array_key_exists('brightness', $options) || array_key_exists('saturation', $options) || array_key_exists('hue', $options)) { + $image = $this->brightness($image, array_key_exists('brightness', $options) ? $options['brightness'] : 1.0, array_key_exists('saturation', $options) ? $options['saturation'] : 1.0, array_key_exists('hue', $options) ? $options['hue'] : 0.0); } - if (isset($options['rotation'])) { + if (array_key_exists('rotation', $options)) { $image = $image->rotate($options['rotation']); } - if (isset($options['flip'])) { + if (array_key_exists('flip', $options)) { if ($options['flip'] == "h") { $image = $image->fliphor(); } @@ -80,44 +130,20 @@ class ImageProcessor $image = $image->flipver(); } } - - $image->writeToFile($target,$saveOptions); + $image->writeToFile($target, $saveOptions); return new \Illuminate\Http\File($target); } - public function createFakeImage($stub,$path, $saveOptions = ['Q' => 100],$options = ["w" => null, "h" => null, "r" => null,"flip" => null , "canv" => null,"rotation" => null]) - { - $image = $this->process($stub,$path,$options,$saveOptions); - } - - - public function brightness($image, float $brightness = 1.0, float $saturation = 1.0, float $hue = 0.0) + public function createFakeImage($stub, $path, $saveOptions = ['Q' => 100], $options = ["w" => null, "h" => null, "r" => null, "flip" => null, "canv" => null, "rotation" => null]) { - $oldInterpretation = $image->interpretation; - $hue %= 360; - if ($hue < 0) { - $hue = 360 + $hue; - } - if ($image->hasAlpha()) { - $imageWithoutAlpha = $image->extract_band(0, ['n' => $image->bands - 1]); - $alpha = $image->extract_band($image->bands - 1, ['n' => 1]); - return $imageWithoutAlpha - ->colourspace(Interpretation::LCH) - ->linear([$brightness, $saturation, 1.0], [0.0, 0.0, $hue]) - ->colourspace($oldInterpretation) - ->bandjoin($alpha); - } - return $image - ->colourspace(Interpretation::LCH) - ->linear([$brightness, $saturation, 1.0], [0.0, 0.0, $hue]) - ->colourspace($oldInterpretation); + return $this->process($stub, $path, $options, $saveOptions); } - public function convertImage(string $filePath,string $target,array $options = ['Q' => 100]) : \Illuminate\Http\File + public function convertImage(string $filePath, string $target, array $options = ['Q' => 100]): \Illuminate\Http\File { $tmpFile = \Jcupitt\Vips\Image::newFromFile($filePath); - $tmpFile->writeToFile($target,$options); + $tmpFile->writeToFile($target, $options); return new \Illuminate\Http\File($target); } } diff --git a/app/Image/Traits/ModulateTrait.php b/app/Image/Traits/ModulateTrait.php new file mode 100644 index 0000000..ae5dfdc --- /dev/null +++ b/app/Image/Traits/ModulateTrait.php @@ -0,0 +1,30 @@ +interpretation; + $hue %= 360; + if ($hue < 0) { + $hue = 360 + $hue; + } + if ($image->hasAlpha()) { + $imageWithoutAlpha = $image->extract_band(0, ['n' => $image->bands - 1]); + $alpha = $image->extract_band($image->bands - 1, ['n' => 1]); + return $imageWithoutAlpha + ->colourspace(Interpretation::LCH) + ->linear([$brightness, $saturation, 1.0], [0.0, 0.0, $hue]) + ->colourspace($oldInterpretation) + ->bandjoin($alpha); + } + return $image + ->colourspace(Interpretation::LCH) + ->linear([$brightness, $saturation, 1.0], [0.0, 0.0, $hue]) + ->colourspace($oldInterpretation); + } + +} diff --git a/app/Models/Collection.php b/app/Models/Collection.php index 2f0cc3d..5582875 100644 --- a/app/Models/Collection.php +++ b/app/Models/Collection.php @@ -39,6 +39,11 @@ class Collection extends Model 'mimetypes' => 'array', ]; + public function files() + { + return $this->hasMany(File::class); + } + protected function exts(): Attribute { return Attribute::make( diff --git a/app/Models/File.php b/app/Models/File.php index eb555d7..830054d 100644 --- a/app/Models/File.php +++ b/app/Models/File.php @@ -8,6 +8,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; class File extends Model @@ -49,5 +50,18 @@ class File extends Model ); } + public function collection() + { + return $this->belongsTo(Collection::class); + } + + public function getPath() + { + return Storage::disk($this->collection->disk)->path($this->server_path . $this->uuid . '.' . $this->collection->ext); + } + public function getModifiedPath() + { + return Storage::disk(app()->collection->disk)->path(app()->file->server_path . app()->file->uuid . '-modified.' . app()->collection->ext); + } } diff --git a/routes/api.php b/routes/api.php index 3c462a3..c0525d6 100644 --- a/routes/api.php +++ b/routes/api.php @@ -31,7 +31,7 @@ Route::get('newTmp/{uuid}', function ($uuid) { Route::group(['as' => 'api.'], function () { Route::apiResource('collections', CollectionController::class); Route::delete('collections/{collection}', [CollectionController::class, "destroy"])->withTrashed(); - // Route::get('{collection_name}/{uuid}.{extention}', [FileController::class, 'show'])->name('files.show'); - Route::get('{collection_name}/{path}', [FileController::class, 'private'])->name('files.private'); + Route::get('{collection_name}/{uuid}.{extention}', [FileController::class, 'show'])->name('files.show'); + // Route::get('{collection_name}/{path}', [FileController::class, 'private'])->name('files.private'); Route::post('{collection_name}/{model_id?}', [FileController::class, 'store'])->name('files.store'); }); diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore index 05c4471..8ef3ff7 100644 --- a/storage/framework/.gitignore +++ b/storage/framework/.gitignore @@ -1,3 +1,4 @@ + compiled.php config.php down diff --git a/storage/stub/image1.png b/storage/stub/image1.png new file mode 100644 index 0000000..977f4e2 Binary files /dev/null and b/storage/stub/image1.png differ diff --git a/storage/stub/image2.jpeg b/storage/stub/image2.jpeg new file mode 100644 index 0000000..bf4e5e2 Binary files /dev/null and b/storage/stub/image2.jpeg differ diff --git a/storage/stub/image3.png b/storage/stub/image3.png new file mode 100644 index 0000000..989ceeb Binary files /dev/null and b/storage/stub/image3.png differ diff --git a/tests/Feature/FileDeleteTest.php b/tests/Feature/FileDeleteTest.php new file mode 100644 index 0000000..2872716 --- /dev/null +++ b/tests/Feature/FileDeleteTest.php @@ -0,0 +1,21 @@ +assertTrue(true); + + } +} diff --git a/tests/Feature/FileShowTest.php b/tests/Feature/FileShowTest.php new file mode 100644 index 0000000..190225f --- /dev/null +++ b/tests/Feature/FileShowTest.php @@ -0,0 +1,20 @@ +assertTrue(true); + } +} diff --git a/tests/Feature/FileStoreTest.php b/tests/Feature/FileStoreTest.php index 3083266..3c7a9c9 100644 --- a/tests/Feature/FileStoreTest.php +++ b/tests/Feature/FileStoreTest.php @@ -31,22 +31,24 @@ class FileStoreTest extends Bootstrap 'tmp_support' => false ]); - $response = $this->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); + $response = $this->loginAs()->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); $response->assertForbidden(); } public function test_tmp_false_and_collection_is_full_forbidden() { + $randomCount = rand(2, 10); $collection = Collection::factory()->createQuietly([ 'tmp_support' => false, 'count' => $randomCount ]); + for ($i = $randomCount; $i > 0; $i--) { $uuid = app()->uuid; $this->one(File::class, [ 'uuid' => $uuid, - 'user_id' => auth()->id(), + 'user_id' => 1, 'collection_id' => $collection->id, 'server_path' => '/' . date('y') . '/' . date('m') . '/' . $uuid . '.' . $collection->ext, ]); @@ -58,7 +60,7 @@ class FileStoreTest extends Bootstrap "description" => 'lfjdsklfslfsdlfasdfsfhgsfgsdf', "public" => 1 ]; - $response = $this->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); + $response = $this->loginAs()->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); $response->assertForbidden(); } @@ -74,7 +76,7 @@ class FileStoreTest extends Bootstrap "public" => 1 ]; - $response = $this->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); + $response = $this->loginAs()->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); $response->assertForbidden(); } @@ -94,7 +96,7 @@ class FileStoreTest extends Bootstrap "public" => 1 ]; - $response = $this->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); + $response = $this->loginAs()->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); $response->assertForbidden(); } @@ -104,7 +106,7 @@ class FileStoreTest extends Bootstrap public function test_store_dynamic_validation_unprocessable($collectionFields, $dataFields) { $collection = Collection::factory()->create($collectionFields); - $response = $this->postJson(route('api.files.store', ['collection_name' => $collection->name]), $dataFields); + $response = $this->loginAs()->postJson(route('api.files.store', ['collection_name' => $collection->name]), $dataFields); $response->assertUnprocessable(); } @@ -143,7 +145,7 @@ class FileStoreTest extends Bootstrap "public" => 1 ]; - $response = $this->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); + $response = $this->loginAs()->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); $response->assertUnprocessable(); } @@ -171,7 +173,7 @@ class FileStoreTest extends Bootstrap "file" => UploadedFile::fake()->image('test.png'), "public" => 1 ]; - $response = $this->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); + $response = $this->loginAs()->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); $response->assertCreated(); } @@ -202,7 +204,7 @@ class FileStoreTest extends Bootstrap "public" => 1 ]; - $response = $this->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); + $response = $this->loginAs()->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); $response->assertCreated(); } @@ -211,12 +213,13 @@ class FileStoreTest extends Bootstrap $collection = Collection::factory()->createQuietly([ 'alt_required' => false, 'description_required' => false, - 'tmp_support' => true, + 'tmp_support' => false, 'max_width' => 2000, 'max_height' => 2000, 'min_width' => 1, 'min_height' => 1, - 'min_file_size' => 0 + 'min_file_size' => 0, + 'count' => 1 ]); $uuid = app()->uuid; $file = File::factory()->createQuietly([ @@ -233,7 +236,7 @@ class FileStoreTest extends Bootstrap "public" => 1 ]; - $response = $this->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); + $response = $this->loginAs()->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); $response->assertCreated(); } @@ -242,12 +245,13 @@ class FileStoreTest extends Bootstrap $collection = Collection::factory()->createQuietly([ 'alt_required' => false, 'description_required' => false, - 'tmp_support' => true, + 'tmp_support' => false, 'max_width' => 2000, 'max_height' => 2000, 'min_width' => 1, 'min_height' => 1, - 'min_file_size' => 0 + 'min_file_size' => 0, + 'count' => 1 ]); $uuid = app()->uuid; $file = File::factory()->createQuietly([ @@ -264,7 +268,7 @@ class FileStoreTest extends Bootstrap "public" => 1 ]; - $response = $this->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); + $response = $this->loginAs()->postJson(route('api.files.store', ['collection_name' => $collection->name]), $data); $response->assertCreated(); } diff --git a/tests/Feature/FileUpdateTest.php b/tests/Feature/FileUpdateTest.php new file mode 100644 index 0000000..6ddc664 --- /dev/null +++ b/tests/Feature/FileUpdateTest.php @@ -0,0 +1,21 @@ +assertTrue(true); + + } +}