Repository: ahmed-aliraqi/laravel-media-uploader Branch: master Commit: 529ea15a18c4 Files: 37 Total size: 65.2 KB Directory structure: gitextract_84fnq49y/ ├── .editorconfig ├── .gitattributes ├── .github/ │ └── workflows/ │ └── tests.yml ├── .gitignore ├── .styleci.yml ├── README.md ├── _ide_helper.php ├── changelog.md ├── composer.json ├── phpunit.xml ├── src/ │ ├── Config/ │ │ └── laravel-media-uploader.php │ ├── Console/ │ │ └── TemporaryClearCommand.php │ ├── Database/ │ │ └── Migrations/ │ │ └── 2020_06_03_131044_create_temporary_files_table.php │ ├── Entities/ │ │ ├── Concerns/ │ │ │ └── HasUploader.php │ │ └── TemporaryFile.php │ ├── Http/ │ │ ├── Controllers/ │ │ │ └── MediaController.php │ │ └── Requests/ │ │ └── MediaRequest.php │ ├── Listeners/ │ │ └── ProcessUploadedMedia.php │ ├── Providers/ │ │ ├── EventServiceProvider.php │ │ ├── RouteServiceProvider.php │ │ └── UploaderServiceProvider.php │ ├── Resources/ │ │ └── lang/ │ │ ├── ar/ │ │ │ └── validation.php │ │ └── en/ │ │ └── validation.php │ ├── Routes/ │ │ └── api.php │ ├── Rules/ │ │ └── MediaRule.php │ ├── Support/ │ │ ├── FFmpegDriver.php │ │ └── Uploader.php │ └── Transformers/ │ └── MediaResource.php └── tests/ ├── Feature/ │ └── UploaderFeatureTest.php ├── Models/ │ └── Blog.php ├── TestCase.php ├── Unit/ │ └── UploaderUnitTest.php ├── config/ │ ├── laravel-media-uploader.php │ └── media-library.php └── database/ └── migrations/ ├── 2020_06_03_131044_create_temporary_files_table.php ├── 2020_06_03_131049_create_blogs_table.php └── 2020_06_26_194753_create_media_table.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ ; This file is for unifying the coding style for different editors and IDEs. ; More information at http://editorconfig.org root = true [*] charset = utf-8 indent_size = 4 indent_style = space end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.blade.php] insert_final_newline = false trim_trailing_whitespace = false [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitattributes ================================================ # Path-based git attributes # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html # Ignore all test and documentation with "export-ignore". /.github export-ignore /.gitattributes export-ignore /.gitignore export-ignore /.travis.yml export-ignore /phpunit.xml.dist export-ignore /.styleci.yml export-ignore /tests export-ignore /.editorconfig export-ignore /changelog.md export-ignore /phpunit.xml export-ignore /README.md export-ignore ================================================ FILE: .github/workflows/tests.yml ================================================ name: tests on: push: pull_request: schedule: - cron: '0 0 * * *' jobs: run-tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: php: [8.0, 8.1, 8.2, 8.3] laravel: [9.*] dependency-version: [prefer-lowest, prefer-stable] include: - laravel: 9.* testbench: 7.* laravel-medialibrary: 10.* name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} steps: - name: Update apt run: sudo apt-get update --fix-missing - name: Install ffmpeg run: sudo apt-get install ffmpeg - name: Checkout code uses: actions/checkout@v2 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick coverage: none - name: Setup Problem Matches run: | echo "::add-matcher::${{ runner.tool_cache }}/php.json" echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Fix Imagick Policy run: sudo sed -i 's/none/read|write/g' /etc/ImageMagick-6/policy.xml - name: Install dependencies run: | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction --no-suggest - name: Execute tests run: vendor/bin/phpunit env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} AWS_BUCKET: ${{ secrets.AWS_BUCKET }} ================================================ FILE: .gitignore ================================================ .idea vendor .phpunit.result.cache composer.lock ================================================ FILE: .styleci.yml ================================================ preset: laravel ================================================ FILE: README.md ================================================ # Laravel Media Uploader

Build Status StyleCI Total Downloads Latest Stable Version License

> This package used to upload files using laravel-media-library before saving model. ![Uploader](https://github.com/ahmed-aliraqi/laravel-file-uploader/blob/master/screenshots/uploader-v2.gif?raw=true) > In this package all uploaded media will be processed. * All videos will converted to `mp4`. * All audios will converted to `mp3`. * All images `width` & `height` & `ratio` will be saved as custom property. * All videos & audios `duration` will be saved as custom property. #### Requirements - PHP >= 7.4 - You should be ensured that the [ffmpeg](https://ffmpeg.org) was installed on your server #### Installation ```bash composer require ahmed-aliraqi/laravel-media-uploader ``` > The package will automatically register a service provider. > You need to publish and run the migration: ```bash php artisan vendor:publish --provider="AhmedAliraqi\LaravelMediaUploader\Providers\UploaderServiceProvider" --tag="migrations" php artisan migrate ``` > Publish [laravel-media-library](https://github.com/spatie/laravel-medialibrary) migrations: ```bash php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="migrations" php artisan migrate ``` > If you want to customize `attachments` validation rules, you should publish the config file: ```bash php artisan vendor:publish --provider="AhmedAliraqi\LaravelMediaUploader\Providers\UploaderServiceProvider" --tag="config" ``` > If you want to customize validation translations, you should publish the `lang` files: ```bash php artisan vendor:publish --provider="AhmedAliraqi\LaravelMediaUploader\Providers\UploaderServiceProvider" --tag="uploader:translations" ``` > This is the default content of the config file: ```php true, 'documents_mime_types' => [ 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .doc & .docx 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .ppt & .pptx 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xls & .xlsx 'text/plain', 'application/pdf', 'application/zip', 'application/x-rar', 'application/x-rar-compressed', 'application/octet-stream', ], ]; ``` > Use `HasUploader` trait in your model: ```php In your controller use `addAllMediaFromTokens()` method to assign the uploaded media to model using the generated tokens: ```php class BlogController extends Controller { public function store(Request $request) { $blog = Blog::create($request->all()); $blog->addAllMediaFromTokens(); return back(); } } ``` > If you do not add any arguments in `addAllMediaFromTokens()` method will add all tokens in `request('media')` with any collection. > >If you want to save specific collection name add it to the second argument. ```php // specified collection name $blog->addAllMediaFromTokens([], 'pictures'); // specified tokens $blog->addAllMediaFromTokens($request->input('tokens', []), 'pictures'); ``` #### Front End Basic Usage ```blade
``` ###### Or Install Component Via NPM ```bash npm i laravel-file-uploader --save-dev ``` > Now you should register the component in your `resources/js/app.js`: ```js // app.js import FileUploader from 'laravel-file-uploader'; Vue.use(FileUploader); ``` #### Usage ```blade ``` ##### Attributes | Attribute |Rule | Type |Description | |--|--|--|--| | media | optional - default: `[]` |array | used to display an existing files | | unlimited |optional - default:`false`| boolean| upload unlimited files - if let it `false` will not be multiple select| | max|optional - default:`12`| int| the maximum uploaded files - if `1` will not me multiple select| |accept| optional - default: `*`| string| the accepted mime types| |form| optional - default: `false`| string| the form id of the uploaded media| |notes| optional - default `null`| string| the help-block that will be displayed under the files| |label| optional - default `null`| string| the label of the uploader| |collection| optional - default `default`|string| the media library collection that the file will store in| |tokens| optional - default: `[]`|array|the recently uploaded files tokens, used to display recently uploaded files in validation case| |max-width| optional - default: `1200`|string|The maximum width of uploaded image| |max-height| optional - default: `1200`|string|The maximum height of uploaded image| #### API * Upload Files * endpoint: /api/uploader/media/upload * method: POST * body: * files[]: multipart form data * response: * ![upload response](https://i.imgur.com/dvPX9Wa.png) * Display Recently Uploaded Files * endpoint: /api/uploader/media * method: GET * params: * tokens[]: temporary token * response: * ![response](https://i.imgur.com/0xaaDPK.png) * Delete Files * endpoint: /api/uploader/media/{id} * method: DELETE * response: * ![response](https://i.imgur.com/dghxe47.png) ================================================ FILE: _ide_helper.php ================================================ */ /** * @method static \AhmedAliraqi\LaravelMediaUploader\Forms\Components\ImageComponent image($name = null) * @method static \AhmedAliraqi\LaravelMediaUploader\Forms\Components\AudioComponent audio($name = null) * @method static \AhmedAliraqi\LaravelMediaUploader\Forms\Components\VideoComponent video($name = null) * @method static \AhmedAliraqi\LaravelMediaUploader\Forms\Components\MediaComponent media($name = null) */ class BsForm extends \Laraeast\LaravelBootstrapForms\Facades\BsForm {} ================================================ FILE: changelog.md ================================================ # Release Notes for Laravel Media Uploader ### v8.0.0 * **Fixes** - Remove support for BsForm package [2d17c04](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/2d17c04831f52838a7c1bc9157e57940030f81a8) ### v6.3.3 * **Fixes** - Run the artisan command once after saving the media instead of running in loop [22](https://github.com/ahmed-aliraqi/laravel-media-uploader/pull/22) by [AbdullahFaqeir](https://github.com/AbdullahFaqeir) - Fix base64 validation issue [05fed33](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/05fed333a5b96196cc78b4fa4aa1e533aef4f1e9) ### v6.3.0 * **Added** - Add Support for laravel 9.x [18](https://github.com/ahmed-aliraqi/laravel-media-uploader/pull/18) by [AbdullahFaqeir](https://github.com/AbdullahFaqeir) ### v6.2.0 * **Fixes** - Clean the temporary files every six hours [b132699](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/b1326999f3cac6a548bad11c00cf2d7da0287b0d) * **Added** - Add Uploader helper [4e743bf](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/4e743bfefdcf03e6d9b3e0d05966f2c08e71ddda) ### v6.1.2 * **Fixes** - Replace Hindu-Arabic numerals to Arabic numerals [4e45b49](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/4e45b4945a8311eecb53e3fd26062934b43aeea4) ### v6.1.0 * **Added** - Optimize and Upload images as base64. ### v6.0.1 * **Fixes** - Add ability to filter by collection with token to avoid duplicate the old media. ### v6.0.0 * **Added** - Add support for php 8.0 * **Changes** - Use `MediaHasBeenAdded` Event instead of `PerformConversionsJob` - Bump `laravel media library` from ^8.0 to ^9.0 ### v5.1.0 * **Added** - Add `AudioComponent` for `laravel-bootstrap-forms`, - Add `VideoComponent` for `laravel-bootstrap-forms`, - Add `Audioomponent` for `laravel-bootstrap-forms`, - Add `Mediaomponent` for `laravel-bootstrap-forms`, - Add `_ide_helper.php` file to provide autocomplete information to your IDE. ### v5.0.1 * **Fixes** - Keep only configured latest media [4e3f6e6](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/4e3f6e6c4b25797fafa1cae3173e89a93e260339). ### v5.0.0 * **Added** - Add `form` and `unlimited` option to form component. ### v4.1.1 * **Fixes** - Fix support for laravel 8.x [678f06d](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/678f06d8441c2cbd8923bc3f0c6aa7b831c36f78) ### v4.1.0 * **Added** - Add support for laravel 8.x ### v4.0.0 * **Changes** - Upgraded media-library to ^8.0. [c7f1e8e](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/c7f1e8eda602d4b377cb33c98cf244c200dd1cf1) ### v3.0.0 * **Changes** - change vendor name of laravel-bootstrap-forms. [d09599d](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/d09599d07d8e6ca92f393de0dd0a47cc1c934b32) ### v2.1.0 * **Added** - Add "regenerate-after-assigning" option in config file [4fb569b](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/4fb569ba99dafd3098698e4aa274c1868d0d9206) * **Changes** - The first argument of `addAllMediaFromTokens($tokens)` now support `string`, `array`, `null` value. [a451ebb](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/a451ebbdfac6e94ca1c588977a4ada4c489a48bf) ### v2.0.1 * **Fixes** - Register and publish translations [b5b7dd3](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/b5b7dd3efd11a6c0c6aeac82e83003da645a1a09) ### v2.0.0 * **Changes** - Remove built in migration and use published instead [8611ac6](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/8611ac6bbb9b8833c8231ae8d03e4cf1cb7d6866). - Remove `uploader:install` command line [7f0bb58](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/7f0bb58b45f634ba4937ff7cdfee025e8a6e021b). - Optional `preview` flag in MediaResource [e16344d](https://github.com/ahmed-aliraqi/laravel-media-uploader/commit/e16344de7eed1fdd33c33186fc4c0b21df23f835). ### v1.0.1 * **Changes** - Add tow optional arguments in `addAllMediaFromTokens()` - $tokens = [] - $collection = 'default' ================================================ FILE: composer.json ================================================ { "name": "ahmed-aliraqi/laravel-media-uploader", "description": "This package used to upload files using laravel-media-library", "type": "library", "license": "MIT", "authors": [ { "name": "Ahmed Fathy", "email": "aliraqi.dev@gmail.com" } ], "require": { "php": "^7.4|^8.0", "laravel/framework": "~5.7|~5.8|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "spatie/laravel-medialibrary": "^9.0|^10.0|^11.0", "php-ffmpeg/php-ffmpeg": "^1.0" }, "require-dev": { "orchestra/testbench": "~3.0|~5.3|~6.0|~7.0", "mockery/mockery": "^1.4" }, "autoload": { "psr-4": { "AhmedAliraqi\\LaravelMediaUploader\\": "src/" } }, "autoload-dev": { "psr-4": { "AhmedAliraqi\\LaravelMediaUploader\\Tests\\": "tests/" } }, "extra": { "laravel": { "providers": [ "AhmedAliraqi\\LaravelMediaUploader\\Providers\\UploaderServiceProvider" ] } } } ================================================ FILE: phpunit.xml ================================================ src/ tests ================================================ FILE: src/Config/laravel-media-uploader.php ================================================ true, 'documents_mime_types' => [ 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .doc & .docx 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .ppt & .pptx 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xls & .xlsx 'text/plain', 'application/pdf', 'application/zip', 'application/x-rar', 'application/x-rar-compressed', 'application/octet-stream', ], ]; ================================================ FILE: src/Console/TemporaryClearCommand.php ================================================ subHours(6)) ->each(function (TemporaryFile $file) { $file->delete(); }); $this->info( "\nThe temporary files has been cleaned successfully. " .now()->toDateTimeString() ); } } ================================================ FILE: src/Database/Migrations/2020_06_03_131044_create_temporary_files_table.php ================================================ bigIncrements('id'); $table->string('token'); $table->string('collection')->default('default'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('temporary_files'); } } ================================================ FILE: src/Entities/Concerns/HasUploader.php ================================================ where('collection', $collection); } $mediaIds = []; $query->whereIn('token', $tokens) ->each(function (TemporaryFile $file) use (&$mediaIds) { foreach ($file->getMedia($file->collection) as $media) { $media->forceFill([ 'model_type' => $this->getMorphClass(), 'model_id' => $this->getKey(), ])->save(); $mediaIds[] = $media->id; } $file->delete(); }); if (count($mediaIds) > 0 && Config::get('laravel-media-uploader.regenerate-after-assigning')) { Artisan::call('media-library:regenerate', [ '--ids' => implode(',', $mediaIds), '--force' => true, ]); } $collection = $collection ?: 'default'; if ($collectionSizeLimit = optional($this->getMediaCollection($collection))->collectionSizeLimit) { $collectionMedia = $this->refresh()->getMedia($collection); if ($collectionMedia->count() > $collectionSizeLimit) { $this->clearMediaCollectionExcept( $collection, $collectionMedia ->reverse() ->take($collectionSizeLimit) ); } } } /** * Get all the model media of the given collection using "MediaResource". * * @param string $collection * @return \Illuminate\Support\Collection */ public function getMediaResource($collection = 'default') { return collect( MediaResource::collection( $this->getMedia($collection) )->jsonSerialize() ); } } ================================================ FILE: src/Entities/TemporaryFile.php ================================================ addMediaConversion('thumb') ->width(70) ->format('png'); $this->addMediaConversion('small') ->width(120) ->format('png'); $this->addMediaConversion('medium') ->width(160) ->format('png'); $this->addMediaConversion('large') ->width(320) ->format('png'); } } ================================================ FILE: src/Http/Controllers/MediaController.php ================================================ whereIn('token', $tokens); $builder->when(request('collection'), function (Builder $builder) { $builder->where(request()->only('collection')); }); } )->get(); return MediaResource::collection($media); } /** * Store a newly created resource in storage. * * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection * * @throws \Spatie\MediaLibrary\MediaCollections\Exceptions\FileDoesNotExist * @throws \Spatie\MediaLibrary\MediaCollections\Exceptions\FileIsTooBig */ public function store(MediaRequest $request) { /** @var \AhmedAliraqi\LaravelMediaUploader\Entities\TemporaryFile $temporaryFile */ $temporaryFile = TemporaryFile::create([ 'token' => Str::random(60), 'collection' => $request->input('collection', 'default'), ]); if (is_string($request->file) && base64_decode(base64_encode($request->file)) === $request->file) { $temporaryFile->addMediaFromBase64($request->file) ->usingFileName(time().'.png') ->toMediaCollection($temporaryFile->collection); } if ($request->hasFile('file')) { $temporaryFile->addMedia($request->file) ->usingFileName(Uploader::formatName($request->file)) ->toMediaCollection($temporaryFile->collection); } foreach ($request->file('files', []) as $file) { $temporaryFile->addMedia($file) ->usingFileName(Uploader::formatName($file)) ->toMediaCollection($temporaryFile->collection); } return MediaResource::collection( $temporaryFile->getMedia( $temporaryFile->collection ?: 'default' ) )->additional([ 'token' => $temporaryFile->token, ]); } /** * @return \Illuminate\Http\JsonResponse */ public function destroy($media) { $modelClass = Config::get( 'media-library.media_model', \Spatie\MediaLibrary\MediaCollections\Models\Media::class ); $media = $modelClass::findOrFail($media); $media->delete(); return response()->json([ 'message' => 'deleted', ]); } } ================================================ FILE: src/Http/Requests/MediaRequest.php ================================================ ['sometimes', 'required', new MediaRule('image', 'video', 'audio', 'document')], 'files' => ['sometimes', 'required', 'array'], 'files.*' => ['sometimes', 'required', new MediaRule('image', 'video', 'audio', 'document')], 'collection' => ['nullable', 'string'], ]; } /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } } ================================================ FILE: src/Listeners/ProcessUploadedMedia.php ================================================ runningUnitTests()) { return; } if ($event->media->getCustomProperty('status') == 'processed') { // Skipped Processing Media File return; } try { if ($this->isImage($event->media)) { $path = $this->processImage($event->media); } elseif ($this->isDocument($event->media)) { $path = $this->processDocument($event->media); } elseif ($this->isVideo($event->media)) { $path = $this->processVideo($event->media); } elseif ($this->isAudio($event->media)) { $path = $this->processAudio($event->media); } else { $path = null; } $this->processingDone($event->media, $path); } catch (RuntimeException $e) { $this->processingFailed($event->media); } $event->media->setCustomProperty('status', 'processing')->save(); } /** * Determine if the media file is an image. * * @return bool */ protected function isImage(Media $media) { return (new ImageGenerator)->canHandleMime($media->mime_type); } /** * Determine if the media file is a document. * * @return bool */ protected function isDocument(Media $media) { return in_array( $media->mime_type, Config::get('laravel-media-uploader.documents_mime_types') ); } /** * Determine if the media file is a video and initiate the required driver. * * @return bool */ protected function isVideo(Media $media) { return app('ffmpeg-driver')->open($media->getPath()) instanceof Video; } /** * Determine if the media file is an audio and the initiate required driver. * * @return bool */ protected function isAudio(Media $media) { return app('ffmpeg-driver')->open($media->getPath()) instanceof Audio; } /** * Process Image File. * * @return null */ protected function processImage(Media $media) { $image = Image::make($media->getPath())->orientate(); $media ->setCustomProperty('type', 'image') ->setCustomProperty('width', $image->width()) ->setCustomProperty('height', $image->height()) ->setCustomProperty('ratio', (string) round($image->width() / $image->height(), 3)) ->save(); } /** * Process Document File. * * @return null */ protected function processDocument(Media $media) { $media->setCustomProperty('type', 'document')->save(); } /** * Process Video File. * * @return string */ protected function processVideo(Media $media) { $media->setCustomProperty('type', 'video')->save(); $video = app('ffmpeg-driver')->open($media->getPath()); $format = new X264; $format->on('progress', $this->increaseProcessProgress($media)); $format->setAudioCodec('aac'); $format->setAdditionalParameters(['-vf', 'pad=ceil(iw/2)*2:ceil(ih/2)*2']); $video->save($format, $processedFile = $this->generatePathForProcessedFile($media, 'mp4')); return $processedFile; } /** * Process Audio File. * * @return string */ protected function processAudio(Media $media) { $media->setCustomProperty('type', 'audio')->save(); $audio = app('ffmpeg-driver')->open($media->getPath()); $format = new Mp3; $format->on('progress', $this->increaseProcessProgress($media)); $audio->save($format, $processedFile = $this->generatePathForProcessedFile($media, 'mp3')); return $processedFile; } protected function increaseProcessProgress(Media $media): \Closure { return function ( $file, $format, $percentage ) use ($media) { // Progress Percentage is $percentage $media->setCustomProperty('progress', $percentage); $media->save(); }; } /** * @param null $processedFilePath * @return void * * @throws \Exception */ protected function processingDone(Media $media, $processedFilePath = null) { // If the processing does not ended with generating a new file. if (is_null($processedFilePath)) { $media->setCustomProperty('status', 'processed') ->setCustomProperty('progress', 100) ->save(); } else { // New Converted Media Will Be Added $duration = app('ffmpeg-driver') ->getFFProbe() ->format($processedFilePath) ->get('duration'); $media->model ->addMedia($processedFilePath) ->withCustomProperties([ 'type' => $media->getCustomProperty('type'), 'status' => 'processed', 'progress' => 100, 'duration' => $duration, ]) ->preservingOriginal() ->toMediaCollection($media->collection_name); (clone $media)->delete(); } } /** * Mark media status as failed. */ protected function processingFailed(Media $media) { $media->setCustomProperty('status', 'failed')->save(); } /** * @param null $extension * @return string */ protected function generatePathForProcessedFile(Media $media, $extension = null) { $path = $media->getPath(); return pathinfo($path, PATHINFO_DIRNAME) .DIRECTORY_SEPARATOR.pathinfo($path, PATHINFO_FILENAME) .'.processed.'.$extension; } } ================================================ FILE: src/Providers/EventServiceProvider.php ================================================ [ ProcessUploadedMedia::class, ], ]; } ================================================ FILE: src/Providers/RouteServiceProvider.php ================================================ mapApiRoutes(); } /** * Define the "api" routes for the application. * * These routes are typically stateless. * * @return void */ protected function mapApiRoutes() { Route::prefix('api') ->middleware('api') ->namespace($this->namespace) ->group(__DIR__.'/../Routes/api.php'); } } ================================================ FILE: src/Providers/UploaderServiceProvider.php ================================================ registerConfig(); $this->registerTranslations(); $this->publishes([ __DIR__.'/../Database/Migrations' => database_path('/migrations'), ], 'migrations'); $this->commands([ TemporaryClearCommand::class, ]); if (! $this->app->runningUnitTests()) { $this->app->booted(function () { $schedule = $this->app->make(Schedule::class); $schedule->command('temporary:clean')->everySixHours(); }); } } /** * Register the service provider. * * @return void */ public function register() { $this->app->register(RouteServiceProvider::class); $this->app->register(EventServiceProvider::class); $this->app->singleton('ffmpeg-driver', function () { return (new FFmpegDriver)->driver(); }); } /** * Register config. * * @return void */ protected function registerConfig() { $this->publishes([ __DIR__.'/../Config/laravel-media-uploader.php' => config_path('laravel-media-uploader.php'), ], 'config'); $this->mergeConfigFrom( __DIR__.'/../Config/laravel-media-uploader.php', 'laravel-media-uploader' ); } /** * Register translations. * * @return void */ public function registerTranslations() { $this->publishes([ __DIR__.'/../Resources/lang' => resource_path('lang/vendor/uploader'), ], 'uploader:translations'); $this->loadTranslationsFrom(__DIR__.'/../Resources/lang', 'uploader'); } } ================================================ FILE: src/Resources/lang/ar/validation.php ================================================ 'قيمة حقل :attribute غير مدعومة', ]; ================================================ FILE: src/Resources/lang/en/validation.php ================================================ 'The :attribute is not supported.', ]; ================================================ FILE: src/Routes/api.php ================================================ name('uploader.media.index'); Route::post('uploader/media/upload', 'MediaController@store')->name('uploader.media.store'); Route::delete('uploader/media/{media}', 'MediaController@destroy')->name('uploader.media.destroy'); ================================================ FILE: src/Rules/MediaRule.php ================================================ types = $types; } /** * Determine if the validation rule passes. * * @param string $attribute * @param UploadedFile|mixed $value * @return bool */ public function passes($attribute, $value) { if (! $value instanceof UploadedFile && ! $this->isBase64($value)) { return false; } try { $type = $this->getTypeString($value); } catch (RuntimeException $e) { return false; } return in_array($type, $this->types); } /** * Get the validation error message. * * @return string */ public function message() { return trans('uploader::validation.invalid'); } /** * @param UploadedFile|mixed $value */ protected function getTypeString($value): string { if ($this->isBase64($value)) { return 'image'; } $fileFullPath = $value->getRealPath(); if ((new Image)->canHandleMime($value->getMimeType())) { $type = 'image'; } elseif (in_array($value->getMimeType(), $this->documentsMimeTypes())) { $type = 'document'; } else { $type = strtolower(class_basename(get_class( app('ffmpeg-driver')->open($fileFullPath) ))); } return $type; // either: image, video or audio. } /** * The supported mime types for document files. * * @return string[] */ protected function documentsMimeTypes() { return Config::get('laravel-media-uploader.documents_mime_types'); } /** * Determine whither the value is base64 image. * * @return bool */ protected function isBase64($value) { return is_string($value) && base64_decode(base64_encode($value)) === $value; } } ================================================ FILE: src/Support/FFmpegDriver.php ================================================ driver = FFMpeg::create([ 'ffmpeg.binaries' => Config::get('media-library.ffmpeg_path'), 'ffprobe.binaries' => Config::get('media-library.ffprobe_path'), 'timeout' => 3600, 'ffmpeg.threads' => 12, ]); } /** * @return \FFMpeg\FFMpeg */ public function driver() { return $this->driver; } } ================================================ FILE: src/Support/Uploader.php ================================================ getClientOriginalExtension(); $name = trim($file->getClientOriginalName(), $extension); $name = self::replaceNumbers($name); return Str::slug($name).$extension; } /** * Convert arabic & persian decimal to valid decimal. */ public static function replaceNumbers(string $string): string { $newNumbers = range(0, 9); // 1. Persian HTML decimal $persianDecimal = [ '۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹', ]; // 2. Arabic HTML decimal $arabicDecimal = [ '٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩', ]; // 3. Arabic Numeric $arabic = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']; // 4. Persian Numeric $persian = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; $string = str_replace($persianDecimal, $newNumbers, $string); $string = str_replace($arabicDecimal, $newNumbers, $string); $string = str_replace($arabic, $newNumbers, $string); return str_replace($persian, $newNumbers, $string); } } ================================================ FILE: src/Transformers/MediaResource.php ================================================ $this->id, 'url' => $this->getFullUrl(), 'preview' => $this->getPreviewUrl(), 'name' => $this->name, 'file_name' => $this->file_name, 'type' => $this->getType(), 'mime_type' => $this->mime_type, 'size' => $this->size, 'human_readable_size' => $this->human_readable_size, 'details' => $this->mediaDetails(), 'status' => $this->mediaStatus(), 'progress' => $this->when($this->mediaStatus() == 'processing', $this->getCustomProperty('progress')), 'conversions' => $this->when( ($this->isImage() || $this->isVideo()) && ! empty($this->getConversions()), $this->getConversions() ), 'links' => [ 'delete' => [ 'href' => url('api/uploader/media/'.$this->getRouteKey()), 'method' => 'DELETE', ], ], ]; } /** * Get the generated conversions links. * * @return array */ public function getConversions() { $results = []; foreach (array_keys($this->getGeneratedConversions()->toArray()) as $conversionName) { $conversion = ConversionCollection::createForMedia($this->resource) ->first(fn (Conversion $conversion) => $conversion->getName() === $conversionName); if ($conversion) { $results[$conversionName] = $this->getFullUrl($conversionName); } } return $results; } /** * Determine if the media type is video. * * @return bool */ public function isVideo() { return $this->getType() == 'video'; } /** * Determine if the media type is image. * * @return bool */ public function isImage() { return $this->getType() == 'image'; } /** * Determine if the media type is audio. * * @return bool */ public function isAudio() { return $this->getType() == 'audio'; } /** * Get the media type. * * @return mixed|string */ public function getType() { return $this->getCustomProperty('type') ?: $this->type; } /** * Get the preview url. * * @return string|void */ public function getPreviewUrl() { if ($this->getType() == 'image') { return $this->getFullUrl(); } return 'https://cdn.jsdelivr.net/npm/laravel-file-uploader/dist/img/attach.png'; } protected function mediaDetails(): array { $duration = (float) $this->getCustomProperty('duration'); return [ $this->mergeWhen($this->isImage(), [ 'width' => $this->getCustomProperty('width'), 'height' => $this->getCustomProperty('height'), 'ratio' => (float) $this->getCustomProperty('ratio'), ]), 'duration' => $this->when($this->isVideo() || $this->isAudio(), $duration), ]; } /** * @return mixed */ protected function mediaStatus() { return $this->getCustomProperty('status'); } } ================================================ FILE: tests/Feature/UploaderFeatureTest.php ================================================ postJson(url('/api/uploader/media/upload'), [ 'files' => [UploadedFile::fake()->image('thumbnail.jpg', 200)], 'collection' => 'images', ]); $response->assertSuccessful(); $response->assertJsonStructure([ 'data' => [ [ 'id', 'url', 'name', 'file_name', 'type', 'type', 'mime_type', 'size', 'human_readable_size', 'status', 'links', ], ], ]); // Display recently uploaded files via token. $response = $this->getJson( url('/api/uploader/media').'?tokens[]='.$response->json('token') ); $response->assertSuccessful(); $this->assertEquals(1, count($response->json('data'))); } /** @test */ public function it_can_delete_uploaded_files() { Storage::fake('public'); /** @var Blog $blog */ $blog = Blog::create(); $blog->addMedia( UploadedFile::fake() ->create('thumbnail.jpg', 200) )->toMediaCollection(); $this->assertEquals(1, $blog->getMedia()->count()); $this->deleteJson(url('/api/uploader/media/'.$blog->getFirstMedia()->id)); $blog->refresh(); $this->assertEquals(0, $blog->getMedia()->count()); } } ================================================ FILE: tests/Models/Blog.php ================================================ addMediaCollection('default') ->onlyKeepLatest(2); } } ================================================ FILE: tests/TestCase.php ================================================ loadLaravelMigrations(['--database' => 'testbench']); $this->loadMigrationsFrom(__DIR__.'/database/migrations'); Application::starting(function ($artisan) { $artisan->resolveCommands([ RegenerateCommand::class, ]); }); } /** * Load package service provider. * * @param \Illuminate\Foundation\Application $app * @return array */ protected function getPackageProviders($app) { return [ UploaderServiceProvider::class, ]; } /** * Define environment setup. * * @param \Illuminate\Foundation\Application $app * @return void */ protected function getEnvironmentSetUp($app) { $app['config']->set('media-library', require __DIR__.'/config/media-library.php'); $app['config']->set('laravel-media-uploader', require __DIR__.'/config/laravel-media-uploader.php'); // Setup default database to use sqlite :memory: $app['config']->set('database.default', 'testbench'); $app['config']->set('database.connections.testbench', [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]); } } ================================================ FILE: tests/Unit/UploaderUnitTest.php ================================================ addMedia( UploadedFile::fake() ->create('thumbnail.jpg', 200) )->toMediaCollection(); $this->assertInstanceOf(Collection::class, $blog->getMediaResource()); } public function test_add_all_media_from_token() { Storage::fake('public'); /** @var Blog $blog */ $blog = Blog::create(); $tmp = TemporaryFile::create([ 'token' => 123, 'collection' => 'default', ]); $tmp->addMedia( UploadedFile::fake() ->image('thumbnail.jpg', 200) )->toMediaCollection(); $media = $tmp->getFirstMedia('default'); $this->assertEquals($media->model_type, TemporaryFile::class); $this->assertEquals($media->model_id, $tmp->id); $blog->addAllMediaFromTokens([123], 'avatars'); $media->refresh(); $this->assertEquals($media->model_type, TemporaryFile::class); $this->assertEquals($media->model_id, $tmp->id); $blog->addAllMediaFromTokens([123]); $media->refresh(); $this->assertEquals($media->model_type, Blog::class); $this->assertEquals($media->model_id, $blog->id); } /** @test */ public function it_keep_only_configured_latest_media() { $blog = Blog::create(); $blog->addMedia(UploadedFile::fake()->image('thumbnail.jpg', 200))->toMediaCollection(); $this->assertCount(1, $blog->refresh()->getMedia()); $tmp = TemporaryFile::create(['token' => 123, 'collection' => 'default']); $tmp->addMedia(UploadedFile::fake()->image('thumbnail.jpg', 200))->toMediaCollection(); $blog->addAllMediaFromTokens([123]); $this->assertCount(2, $blog->refresh()->getMedia()); $tmp = TemporaryFile::create(['token' => 123, 'collection' => 'default']); $tmp->addMedia(UploadedFile::fake()->image('thumbnail.jpg', 200))->toMediaCollection(); $tmp->addMedia(UploadedFile::fake()->image('thumbnail.jpg', 200))->toMediaCollection(); $blog->addAllMediaFromTokens([123]); $this->assertCount(2, $blog->refresh()->getMedia()); } public function test_uploader_helper() { $this->assertEquals( Str::slug('صورة').'.jpg', Uploader::formatName(UploadedFile::fake()->image('صورة.jpg', 200)) ); $this->assertEquals( '123.jpg', Uploader::formatName(UploadedFile::fake()->image('١٢٣.jpg', 200)) ); } } ================================================ FILE: tests/config/laravel-media-uploader.php ================================================ [ 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .doc & .docx 'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation', // .ppt & .pptx 'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xls & .xlsx 'text/plain', 'application/pdf', 'application/zip', 'application/x-rar', 'application/x-rar-compressed', 'application/octet-stream', ], ]; ================================================ FILE: tests/config/media-library.php ================================================ env('MEDIA_DISK', 'public'), /* * The maximum file size of an item in bytes. * Adding a larger file will result in an exception. */ 'max_file_size' => 1024 * 1024 * 10, /* * This queue will be used to generate derived and responsive images. * Leave empty to use the default queue. */ 'queue_name' => '', /* * By default all conversions will be performed on a queue. */ 'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true), /* * The fully qualified class name of the media model. */ 'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class, /* * The fully qualified class name of the model used for temporary uploads. * * This model is only used in Media Library Pro (https://medialibrary.pro) */ 'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class, /* * When enabled, Media Library Pro will only process temporary uploads there were uploaded * in the same session. You can opt to disable this for stateless usage of * the pro components. */ 'enable_temporary_uploads_session_affinity' => true, /* * When enabled, Media Library pro will generate thumbnails for uploaded file. */ 'generate_thumbnails_for_temporary_uploads' => true, /* * This is the class that is responsible for naming generated files. */ 'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class, /* * The class that contains the strategy for determining a media file's path. */ 'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class, /* * When urls to files get generated, this class will be called. Use the default * if your files are stored locally above the site root or on s3. */ 'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class, /* * Moves media on updating to keep path consistent. Enable it only with a custom * PathGenerator that uses, for example, the media UUID. */ 'moves_media_on_update' => false, /* * Whether to activate versioning when urls to files get generated. * When activated, this attaches a ?v=xx query string to the URL. */ 'version_urls' => false, /* * The media library will try to optimize all converted images by removing * metadata and applying a little bit of compression. These are * the optimizers that will be used by default. */ 'image_optimizers' => [ Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [ '-m85', // set maximum quality to 85% '--strip-all', // this strips out all text information such as comments and EXIF data '--all-progressive', // this will make sure the resulting image is a progressive one ], Spatie\ImageOptimizer\Optimizers\Pngquant::class => [ '--force', // required parameter for this package ], Spatie\ImageOptimizer\Optimizers\Optipng::class => [ '-i0', // this will result in a non-interlaced, progressive scanned image '-o2', // this set the optimization level to two (multiple IDAT compression trials) '-quiet', // required parameter for this package ], Spatie\ImageOptimizer\Optimizers\Svgo::class => [ '--disable=cleanupIDs', // disabling because it is known to cause troubles ], Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [ '-b', // required parameter for this package '-O3', // this produces the slowest but best results ], Spatie\ImageOptimizer\Optimizers\Cwebp::class => [ '-m 6', // for the slowest compression method in order to get the best compression. '-pass 10', // for maximizing the amount of analysis pass. '-mt', // multithreading for some speed improvements. '-q 90', // quality factor that brings the least noticeable changes. ], ], /* * These generators will be used to create an image of media files. */ 'image_generators' => [ Spatie\MediaLibrary\Conversions\ImageGenerators\Image::class, Spatie\MediaLibrary\Conversions\ImageGenerators\Webp::class, Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf::class, Spatie\MediaLibrary\Conversions\ImageGenerators\Svg::class, Spatie\MediaLibrary\Conversions\ImageGenerators\Video::class, ], /* * The path where to store temporary files while performing image conversions. * If set to null, storage_path('media-library/temp') will be used. */ 'temporary_directory_path' => null, /* * The engine that should perform the image conversions. * Should be either `gd` or `imagick`. */ 'image_driver' => env('IMAGE_DRIVER', 'gd'), /* * FFMPEG & FFProbe binaries paths, only used if you try to generate video * thumbnails and have installed the php-ffmpeg/php-ffmpeg composer * dependency. */ 'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'), 'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'), /* * Here you can override the class names of the jobs used by this package. Make sure * your custom jobs extend the ones provided by the package. */ 'jobs' => [ 'perform_conversions' => Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob::class, 'generate_responsive_images' => Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob::class, ], /* * When using the addMediaFromUrl method you may want to replace the default downloader. * This is particularly useful when the url of the image is behind a firewall and * need to add additional flags, possibly using curl. */ 'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class, 'remote' => [ /* * Any extra headers that should be included when uploading media to * a remote disk. Even though supported headers may vary between * different drivers, a sensible default has been provided. * * Supported by S3: CacheControl, Expires, StorageClass, * ServerSideEncryption, Metadata, ACL, ContentEncoding */ 'extra_headers' => [ 'CacheControl' => 'max-age=604800', ], ], 'responsive_images' => [ /* * This class is responsible for calculating the target widths of the responsive * images. By default we optimize for filesize and create variations that each are 20% * smaller than the previous one. More info in the documentation. * * https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images */ 'width_calculator' => Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator::class, /* * By default rendering media to a responsive image will add some javascript and a tiny placeholder. * This ensures that the browser can already determine the correct layout. */ 'use_tiny_placeholders' => true, /* * This class will generate the tiny placeholder used for progressive image loading. By default * the media library will use a tiny blurred jpg image. */ 'tiny_placeholder_generator' => Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred::class, ], /* * When enabling this option, a route will be registered that will enable * the Media Library Pro Vue and React components to move uploaded files * in a S3 bucket to their right place. */ 'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false), /* * When converting Media instances to response the media library will add * a `loading` attribute to the `img` tag. Here you can set the default * value of that attribute. * * Possible values: 'lazy', 'eager', 'auto' or null if you don't want to set any loading instruction. * * More info: https://css-tricks.com/native-lazy-loading/ */ 'default_loading_attribute_value' => null, ]; ================================================ FILE: tests/database/migrations/2020_06_03_131044_create_temporary_files_table.php ================================================ bigIncrements('id'); $table->string('token'); $table->string('collection')->default('default'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('temporary_files'); } } ================================================ FILE: tests/database/migrations/2020_06_03_131049_create_blogs_table.php ================================================ bigIncrements('id'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('blogs'); } } ================================================ FILE: tests/database/migrations/2020_06_26_194753_create_media_table.php ================================================ bigIncrements('id'); $table->morphs('model'); $table->uuid('uuid')->nullable()->unique(); $table->string('collection_name'); $table->string('name'); $table->string('file_name'); $table->string('mime_type')->nullable(); $table->string('disk'); $table->string('conversions_disk')->nullable(); $table->unsignedBigInteger('size'); $table->json('manipulations'); $table->json('custom_properties'); $table->json('generated_conversions'); $table->json('responsive_images'); $table->unsignedInteger('order_column')->nullable(); $table->nullableTimestamps(); }); } /** * Reverse the migrations. */ public function down() { Schema::dropIfExists('media'); } }