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
> This package used to upload files using laravel-media-library before saving model.

> 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:
* 
* Display Recently Uploaded Files
* endpoint: /api/uploader/media
* method: GET
* params:
* tokens[]: temporary token
* response:
* 
* Delete Files
* endpoint: /api/uploader/media/{id}
* method: DELETE
* response:
* 
================================================
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');
}
}