diff --git a/.env.example b/.env.example index d3cf695..e355062 100644 --- a/.env.example +++ b/.env.example @@ -40,11 +40,14 @@ MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" +STORAGE_FOLDER='mytivi' +APP_STORAGE= AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= -AWS_DEFAULT_REGION=us-east-1 +AWS_DEFAULT_REGION= AWS_BUCKET= -AWS_USE_PATH_STYLE_ENDPOINT=false +AWS_USE_PATH_STYLE_ENDPOINT=true +AWS_ENDPOINT= PUSHER_APP_ID= PUSHER_APP_KEY= diff --git a/app/Helper/Common.php b/app/Helper/Common.php new file mode 100644 index 0000000..89c4f6c --- /dev/null +++ b/app/Helper/Common.php @@ -0,0 +1,29 @@ + 'nullable|array', + "$key.*" => ['nullable', 'string', Rule::in(['asc', 'desc'])], + ]; + } + public static function getSearchValidation() { return 'nullable|string'; } + } +?> \ No newline at end of file diff --git a/app/Helper/FileHelper.php b/app/Helper/FileHelper.php index af7eb61..69059a0 100644 --- a/app/Helper/FileHelper.php +++ b/app/Helper/FileHelper.php @@ -13,8 +13,15 @@ class FileHelper { return implode(', ', $allowedFileExtensions); } - static function convertToStrLaraValidation(array $allowedFileExtensions) { - return implode(',', $allowedFileExtensions); + static function convertToStrLaraValidation(array $allowedFileExtensions, $type = 'string') { + $validations = [ + 'mimes:' . implode(',', $allowedFileExtensions), + 'extensions:' . implode(',', $allowedFileExtensions) + ]; + + if($type == 'string') return implode('|', $validations); + else if($type == 'array') return $validations; + else throw new \Exception('Type not valid'); } } ?> \ No newline at end of file diff --git a/app/Helper/Traits/Models/CanMultiOrderBy.php b/app/Helper/Traits/Models/CanMultiOrderBy.php new file mode 100644 index 0000000..c192e6b --- /dev/null +++ b/app/Helper/Traits/Models/CanMultiOrderBy.php @@ -0,0 +1,18 @@ + $direction) { + $queryBuilder->orderBy($column, $direction); + } + } else if($defaultOrderBy) { + $queryBuilder->orderByRaw($defaultOrderBy); + } + } +} diff --git a/app/Helper/Traits/Models/CanMultiSearch.php b/app/Helper/Traits/Models/CanMultiSearch.php new file mode 100644 index 0000000..365db91 --- /dev/null +++ b/app/Helper/Traits/Models/CanMultiSearch.php @@ -0,0 +1,20 @@ +where(function($q) use($search, $searchColumns) { + foreach($searchColumns as $column) { + $q->orWhere($column, 'ilike', "%$search%"); + } + }); + } +} diff --git a/app/Http/Controllers/api/superadmin/VideoUploadController.php b/app/Http/Controllers/api/superadmin/VideoUploadController.php new file mode 100644 index 0000000..cfa045f --- /dev/null +++ b/app/Http/Controllers/api/superadmin/VideoUploadController.php @@ -0,0 +1,30 @@ +validate([ + 'perPage' => 'nullable|integer|min:1', + ...DatabaseHelper::getOrderBysValidations(), + 'search' => DatabaseHelper::getSearchValidation() + ]); + + $data = VideoUpdate::multiSearch($request->search, ['file_name']) + ->multiOrderBy($request->orderBys, 'created_at desc') + ->paginate($request->perPage ?? 10 ); + return JSONResponse::Success(['data' => $data]); + } + + public function save(Request $request) { return VideoUpdate::upsertFromRequest($request); } + public function update(Request $request) { return VideoUpdate::upsertFromRequest($request); } + public function delete(Request $request) { return VideoUpdate::deleteFromRequest($request); } + + public function changeSelectedVideo(Request $request) { return VideoUpdate::changeSelectedVideoFromRequest($request); } +} diff --git a/app/Models/VideoUpdate.php b/app/Models/VideoUpdate.php index 3e93c3d..1bedafe 100644 --- a/app/Models/VideoUpdate.php +++ b/app/Models/VideoUpdate.php @@ -2,11 +2,146 @@ namespace App\Models; +use App\Helper\Common; +use App\Helper\FileHelper; +use App\Helper\JSONResponse; +use App\Helper\Traits\Models\CanMultiOrderBy; +use App\Helper\Traits\Models\CanMultiSearch; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; +use Illuminate\Http\File; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Storage; class VideoUpdate extends Model { use HasFactory; + use CanMultiSearch; + use CanMultiOrderBy; protected $table = 'video_updates'; + protected $hidden = ['file']; + protected $appends = ['file_url']; + + public static function upsertFromRequest(Request $request) { + $request->validate([ + 'id' => 'nullable|integer|exists:App\Models\VideoUpdate,id', + 'is_selected' => 'nullable|in:true,false', + 'file' => 'required_without:id|file|' . FileHelper::convertToStrLaraValidation(FileHelper::$allowedVideoExtensions), + 'file_name' => 'required|string', + ], [ + 'file' => ['required_without' => 'The file field is required.'] + ]); + + $delOldDbFileLocation = ''; + $newDbFileLocation = ''; + try { + // save photo + if($request->file) $newDbFileLocation = self::saveFile($request->file)['db_url']; + + // try to upsert data + DB::beginTransaction(); + $videoUpdate = null; + if(!$request->id) $videoUpdate = new VideoUpdate(); + else $videoUpdate = VideoUpdate::findOrFail($request->id); + + // del old db file location if has old file + if($newDbFileLocation) { + if($videoUpdate->file) $delOldDbFileLocation = $videoUpdate->file; + $videoUpdate->file = $newDbFileLocation; + } + $videoUpdate->file_name = $request->file_name; + + if($request->is_selected == 'true') { + VideoUpdate::where('is_selected', true)->update(['is_selected' => false]); + $videoUpdate->is_selected = true; + } else $videoUpdate->is_selected = false; + + // renew data + $videoUpdate->save(); + + // delete old file if exist + if($delOldDbFileLocation) self::deleteFile($delOldDbFileLocation); + DB::commit(); + return JSONResponse::Success(); + } catch (\Throwable $th) { + DB::rollBack(); + if($newDbFileLocation) self::deleteFile($newDbFileLocation); + throw $th; + } + } + + public static function deleteFromRequest(Request $request) { + $request->validate(['id' => 'required|integer|exists:App\Models\VideoUpdate,id']); + try { + DB::beginTransaction(); + $videoUpdate = VideoUpdate::findOrFail($request->id); + if($videoUpdate->is_selected) throw new \Exception("Cannot delete video when 'is Selected' is true"); + + $oldDbFile = $videoUpdate->file; + $videoUpdate->delete(); + + if($oldDbFile) self::deleteFile($oldDbFile); + DB::commit(); + return JSONResponse::Success(); + } catch (\Throwable $th) { + DB::rollBack(); + throw $th; + } + + } + + public static function changeSelectedVideoFromRequest(Request $request) { + $request->validate(['id' => 'required|integer|exists:App\Models\VideoUpdate,id']); + + try { + DB::beginTransaction(); + + $videoUpdate = VideoUpdate::findOrFail($request->id); + $videoUpdate->is_selected = !$videoUpdate->is_selected; + $videoUpdate->save(); + DB::commit(); + + if($videoUpdate->is_selected) { + VideoUpdate::where([ + ['id', '!=', $videoUpdate->id], + ['is_selected', true] + ])->update(['is_selected' => false]); + } + return JSONResponse::Success(); + } catch (\Throwable $th) { + DB::rollBack(); + throw $th; + } + } + + // -- File UTILITIES + protected function fileUrl(): Attribute { + return Attribute::make( + fn() => $this->file ? Storage::disk('s3')->url($this->file) : '' + ); + } + private static function fileFolder() { return env('STORAGE_FOLDER', 'tivi') . '/video-upload'; } + + public static function saveFile($file) { + if (!$file->isValid()) throw new \Exception('File is not valid'); + + //Save file to local data + $fileName = self::getFileName($file); + $path = self::fileFolder(); + + Storage::disk('s3')->put($path . '/' . $fileName, file_get_contents(new File($file)), 'public'); + return ['db_url' => "$path/$fileName"]; + } + private static function getFileName($file) { + $tz = Carbon::now()->timestamp; + $extension = $file->getClientOriginalExtension(); + $name = "$tz-" . Common::generateRandomString(); + + return "$name.$extension"; + } + public static function deleteFile($dbUrl) { Storage::disk('s3')->delete($dbUrl); } + // -- END File UTILITIES } diff --git a/composer.json b/composer.json index 712ab7e..b56b240 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "guzzlehttp/guzzle": "^7.2", "laravel/framework": "^11.0", "laravel/sanctum": "^4.0", - "laravel/tinker": "^2.8" + "laravel/tinker": "^2.8", + "league/flysystem-aws-s3-v3": "3.0" }, "require-dev": { "fakerphp/faker": "^1.9.1", diff --git a/composer.lock b/composer.lock index 003784b..feac24f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,157 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "de9bbe912febcc4d52bea5bd480b1397", + "content-hash": "f180282c8ac03d8abf741f87e8b0e4c7", "packages": [ + { + "name": "aws/aws-crt-php", + "version": "v1.2.4", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/eb0c6e4e142224a10b08f49ebf87f32611d162b2", + "reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.4" + }, + "time": "2023-11-08T00:42:13+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.304.8", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "0079eaa0a0eaef2d73d0a4a11389cdfce1d33189" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0079eaa0a0eaef2d73d0a4a11389cdfce1d33189", + "reference": "0079eaa0a0eaef2d73d0a4a11389cdfce1d33189", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "guzzlehttp/promises": "^1.4.0 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "mtdowling/jmespath.php": "^2.6", + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.304.8" + }, + "time": "2024-04-19T18:13:09+00:00" + }, { "name": "brick/math", "version": "0.11.0", @@ -1775,6 +1924,62 @@ ], "time": "2024-04-07T19:17:50+00:00" }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "f8ba6a92a5c1fdcbdd89dede009a1e6e1b93ba8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/f8ba6a92a5c1fdcbdd89dede009a1e6e1b93ba8c", + "reference": "f8ba6a92a5c1fdcbdd89dede009a1e6e1b93ba8c", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.132.4", + "league/flysystem": "^2.0.0 || ^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3V3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "AWS S3 filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "aws", + "file", + "files", + "filesystem", + "s3", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues", + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.0.0" + }, + "time": "2022-01-13T21:11:49+00:00" + }, { "name": "league/flysystem-local", "version": "3.25.1", @@ -1991,6 +2196,72 @@ ], "time": "2024-04-12T21:02:21+00:00" }, + { + "name": "mtdowling/jmespath.php", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/bbb69a935c2cbb0c03d7f481a238027430f6440b", + "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.7.0" + }, + "time": "2023-08-25T10:54:48+00:00" + }, { "name": "nesbot/carbon", "version": "3.2.4", diff --git a/routes/api.php b/routes/api.php index 5e2846e..884c484 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,6 +1,7 @@ group(function() { Route::post('/auth/logout', 'logout'); }); }); + +Route::middleware(USER_MIDDLEWARES)->prefix('superadmin')->group(function() { + Route::controller(VideoUploadController::class)->group(function() { + Route::post('/video-upload', 'init'); + Route::post('/video-upload/save', 'save'); + Route::post('/video-upload/update', 'update'); + Route::post('/video-upload/delete', 'delete'); + Route::post('/video-upload/change-selected-video', 'changeSelectedVideo'); + }); +}); +// tmux session, tmux attach session -t \ No newline at end of file