Compare commits

..

2 Commits

Author SHA1 Message Date
philipp lang 4a87b83b5a Add pathinfo information to default Properties 2024-10-30 15:25:43 +01:00
philipp lang 09919a1c29 Add heic support 2024-04-30 14:13:21 +02:00
42 changed files with 1908 additions and 3713 deletions

View File

@ -6,7 +6,7 @@ This package creates routes for the popular Medialibrary Package from Spatie ().
In RegisterMediaCollections, you have the following methods available:
You can set a filename by default for the file. This accepts the associated Model, as well as the original basename (without extension). You should return the new name of the file without the extension (e.g. disc).
You can set a filename by default for the file. This accepts the associated Model, as well as the original filename. You should return the new name of the file with the extension (e.g. disc.jpg).
```
forceFileName(fn ($model, $path) => Str::slug($path))
@ -25,3 +25,4 @@ You can call whatever you want after an image has been added, modified or delete
....
})
```

View File

@ -1,14 +0,0 @@
<template>
<svg x="0px" y="0px" viewBox="0 0 511.626 511.627" xml:space="preserve" fill="currentColor">
<g>
<g>
<path
d="M392.857,292.354h-18.274c-2.669,0-4.859,0.855-6.563,2.573c-1.718,1.708-2.573,3.897-2.573,6.563v91.361 c0,12.563-4.47,23.315-13.415,32.262c-8.945,8.945-19.701,13.414-32.264,13.414H82.224c-12.562,0-23.317-4.469-32.264-13.414 c-8.945-8.946-13.417-19.698-13.417-32.262V155.31c0-12.562,4.471-23.313,13.417-32.259c8.947-8.947,19.702-13.418,32.264-13.418 h200.994c2.669,0,4.859-0.859,6.57-2.57c1.711-1.713,2.566-3.9,2.566-6.567V82.221c0-2.662-0.855-4.853-2.566-6.563 c-1.711-1.713-3.901-2.568-6.57-2.568H82.224c-22.648,0-42.016,8.042-58.102,24.125C8.042,113.297,0,132.665,0,155.313v237.542 c0,22.647,8.042,42.018,24.123,58.095c16.086,16.084,35.454,24.13,58.102,24.13h237.543c22.647,0,42.017-8.046,58.101-24.13 c16.085-16.077,24.127-35.447,24.127-58.095v-91.358c0-2.669-0.856-4.859-2.574-6.57 C397.709,293.209,395.519,292.354,392.857,292.354z"
/>
<path
d="M506.199,41.971c-3.617-3.617-7.905-5.424-12.85-5.424H347.171c-4.948,0-9.233,1.807-12.847,5.424 c-3.617,3.615-5.428,7.898-5.428,12.847s1.811,9.233,5.428,12.85l50.247,50.248L198.424,304.067 c-1.906,1.903-2.856,4.093-2.856,6.563c0,2.479,0.953,4.668,2.856,6.571l32.548,32.544c1.903,1.903,4.093,2.852,6.567,2.852 s4.665-0.948,6.567-2.852l186.148-186.148l50.251,50.248c3.614,3.617,7.898,5.426,12.847,5.426s9.233-1.809,12.851-5.426 c3.617-3.616,5.424-7.898,5.424-12.847V54.818C511.626,49.866,509.813,45.586,506.199,41.971z"
/>
</g>
</g>
</svg>
</template>

View File

@ -1,105 +0,0 @@
<template>
<div class="space-y-2">
<div v-if="label" class="text-sm font-semibold text-gray-400" v-text="label"></div>
<label class="flex items-center justify-center h-[35px] border-2 border-solid border-gray-600 rounded-lg text-sm text-gray-300 bg-gray-700 relative" :for="id">
<div class="relative">Klicken oder Datei hierhin ziehen zum hochladen</div>
<input :id="id" ref="uploader" class="hidden" type="file" :name="name" multiple @change="upload($event.target.files)" />
<div class="absolute w-full h-full top-0 left-0 cursor-pointer" @drop="onDropping" @dragenter="onDragEnter" @dragover="onDragOver" @dragleave="onDragLeave"></div>
</label>
<div v-for="file in inner" :key="file.id" class="flex h-[35px] justify-between items-center space-x-2 px-2 border-2 border-solid border-gray-600 rounded-lg text-gray-300 bg-gray-700">
<img :src="file.icon" class="w-6 h-6" />
<div class="text-sm text-gray-300 leading-none grow" v-text="file.file_name"></div>
<slot name="buttons" button-class="flex justify-center items-center w-6 h-6 rounded-full" icon-class="w-3 h-3" :file="file"></slot>
<a v-tooltip="`Löschen`" href="#" class="flex justify-center items-center w-6 h-6 rounded-full bg-red-200" @click.prevent="onDelete(file)">
<trash-icon class="text-red-800 w-3 h-3"></trash-icon>
</a>
<a v-tooltip="`Öffnen`" :href="file.original_url" class="flex justify-center items-center w-6 h-6 rounded-full bg-primary-700" target="_BLANK">
<external-icon class="text-primary-200 w-3 h-3"></external-icon>
</a>
</div>
</div>
</template>
<script setup>
import {ref, inject} from 'vue';
import TrashIcon from './TrashIcon.vue';
import ExternalIcon from './ExternalIcon.vue';
import useReadFile from '../composables/useReadFile.js';
import {useToast} from 'vue-toastification';
const emit = defineEmits(['update:modelValue']);
const axios = inject('axios');
const toast = useToast();
const queue = ref([]);
const inner = ref([]);
const {onDragEnter, onDragOver, onDragLeave, processDrop, read} = useReadFile();
const props = defineProps({
collection: {
required: true,
type: String,
},
parentName: {
required: true,
type: String,
},
parentId: {
required: true,
validator: (value) => typeof value === 'number' || value === null,
},
name: {
type: String,
required: true,
},
id: {
required: true,
type: String,
},
label: {
required: false,
default: () => null,
validator: (value) => value === null || typeof value === 'string',
},
});
async function onDropping(e) {
await upload(await processDrop(e, 1));
}
async function upload(files) {
[...files].forEach((f) => realUpload(f));
}
async function realUpload(file) {
var payload = await read(file);
var identifier = Math.random().toString(36) + Date.now() + payload.content.substring(2, 15);
queue.value = [...queue.value, {file_name: payload.name, size: payload.size, identifier: identifier}];
var response = await axios.post('/mediaupload', {
parent: {model: props.parentName, collection_name: props.collection, id: props.parentId ? props.parentId : null},
payload: [payload],
});
queue.value = queue.value.filter((f) => f.identifier !== identifier);
inner.value.push(response.data[0]);
emit('update:modelValue', inner.value);
toast.success('Dateien hochgeladen');
}
async function onDelete(file) {
await axios.delete(`/mediaupload/${file.id}`);
inner.value = inner.value.filter((f) => f.id !== file.id);
emit('update:modelValue', inner.value);
toast.success('Datei entfernt');
}
async function reload() {
var response = await axios.get(`/mediaupload/${props.parentName}/${props.parentId}/${props.collection}`);
inner.value = response.data;
emit('update:modelValue', response.data);
}
if (props.parentId !== null) {
reload();
}
</script>

View File

@ -1,6 +0,0 @@
<template>
<svg height="426.66667pt" viewBox="0 0 426.66667 426.66667" width="426.66667pt" xmlns="http://www.w3.org/2000/svg">
<path
d="m405.332031 192h-170.664062v-170.667969c0-11.773437-9.558594-21.332031-21.335938-21.332031-11.773437 0-21.332031 9.558594-21.332031 21.332031v170.667969h-170.667969c-11.773437 0-21.332031 9.558594-21.332031 21.332031 0 11.777344 9.558594 21.335938 21.332031 21.335938h170.667969v170.664062c0 11.777344 9.558594 21.335938 21.332031 21.335938 11.777344 0 21.335938-9.558594 21.335938-21.335938v-170.664062h170.664062c11.777344 0 21.335938-9.558594 21.335938-21.335938 0-11.773437-9.558594-21.332031-21.335938-21.332031zm0 0" />
</svg>
</template>

View File

@ -1,117 +0,0 @@
<template>
<label class="flex flex-col" :for="id">
<span v-if="label" class="text-sm font-semibold text-gray-400">
{{ label }}
<span v-show="required" class="text-red-800">&nbsp;*</span>
</span>
<div class="h-[35px] border-2 border-solid relative rounded-lg cursor-pointer flex-none border-gray-600 text-gray-300 bg-gray-700">
<div v-if="inner === null" class="flex items-center justify-center h-full">
<div class="relative text-sm text-gray-300 leading-none">Klicken oder Datei hierhin ziehen zum hochladen</div>
<input :id="id" class="hidden" type="file" :name="name" :multiple="false" @change="upload($event.target.files)" />
<div class="absolute w-full h-full top-0 left-0 cursor-pointer" @drop="onDropping" @dragenter="onDragEnter" @dragover="onDragOver" @dragleave="onDragLeave"></div>
</div>
<div v-else class="flex justify-between items-center h-full space-x-2 px-2">
<img :src="inner.icon" class="w-6 h-6" />
<div class="text-sm text-gray-300 leading-none grow" v-text="inner.file_name"></div>
<a v-tooltip="`Löschen`" href="#" class="flex justify-center items-center w-6 h-6 rounded-full bg-red-200" @click.prevent="onDelete">
<trash-icon class="text-red-800 w-3 h-3"></trash-icon>
</a>
<a v-tooltip="`Öffnen`" :href="inner.original_url" class="flex justify-center items-center w-6 h-6 rounded-full bg-primary-700" target="_BLANK">
<external-icon class="text-primary-200 w-3 h-3"></external-icon>
</a>
</div>
</div>
</label>
</template>
<script setup>
import {watch, ref, inject} from 'vue';
import TrashIcon from './TrashIcon.vue';
import ExternalIcon from './ExternalIcon.vue';
import useReadFile from '../composables/useReadFile.js';
import {useToast} from 'vue-toastification';
const emit = defineEmits(['update:modelValue']);
const axios = inject('axios');
const toast = useToast();
const inner = ref(null);
const {dropping, onDragEnter, onDragOver, onDragLeave, processDrop, read} = useReadFile();
const props = defineProps({
required: {
type: Boolean,
default: () => false,
},
collection: {
required: true,
type: String,
},
parentName: {
required: true,
type: String,
},
parentId: {
required: true,
validator: (value) => typeof value === 'number' || value === null,
},
name: {
type: String,
required: true,
},
id: {
required: true,
type: String,
},
label: {
required: false,
default: () => null,
validator: (value) => value === null || typeof value === 'string',
},
});
function parseValue(v) {
if (v === null) {
return null;
}
if (v.fallback) {
return null;
}
return v;
}
async function reload() {
var response = await axios.get(`/mediaupload/${props.parentName}/${props.parentId}/${props.collection}`);
inner.value = parseValue(response.data);
emit('update:modelValue', inner.value.id);
}
async function onDropping(e) {
await upload(await processDrop(e, 1));
}
async function upload(files) {
await realUpload(await read(files[0]));
}
async function realUpload(file) {
inner.value = (
await axios.post('/mediaupload', {
parent: {model: props.parentName, collection_name: props.collection, id: props.parentId ? props.parentId : null},
payload: file,
})
).data;
emit('update:modelValue', inner.value);
toast.success('Datei hochgeladen');
}
async function onDelete() {
await axios.delete(`/mediaupload/${inner.value.id}`);
inner.value = null;
emit('update:modelValue', null);
toast.success('Datei entfernt');
}
if (props.parentId !== null) {
reload();
}
</script>

View File

@ -1,35 +0,0 @@
<template>
<svg
version="1.1"
id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 512 512"
style="enable-background: new 0 0 512 512"
xml:space="preserve"
fill="currentColor"
>
<g>
<g>
<path
d="M486.4,102.4h-128V25.6c0-15.36-10.24-25.6-25.6-25.6H179.2c-15.36,0-25.6,10.24-25.6,25.6v76.8h-128
C10.24,102.4,0,112.64,0,128s10.24,25.6,25.6,25.6h460.8c15.36,0,25.6-10.24,25.6-25.6S501.76,102.4,486.4,102.4z M307.2,102.4
H204.8V51.2h102.4V102.4z"
/>
</g>
</g>
<g>
<g>
<path
d="M25.6,204.8l48.64,284.16c2.56,12.8,12.8,23.04,25.6,23.04h312.32c12.8,0,23.04-10.24,25.6-23.04L486.4,204.8H25.6z
M153.6,460.8c-15.36,0-25.6-10.24-25.6-25.6l-25.6-153.6c0-15.36,10.24-25.6,25.6-25.6s25.6,10.24,25.6,25.6l25.6,153.6
C179.2,450.56,168.96,460.8,153.6,460.8z M281.6,435.2c0,15.36-10.24,25.6-25.6,25.6s-25.6-10.24-25.6-25.6V281.6
c0-15.36,10.24-25.6,25.6-25.6s25.6,10.24,25.6,25.6V435.2z M384,435.2c0,15.36-10.24,25.6-25.6,25.6
c-15.36,0-25.6-10.24-25.6-25.6l25.6-153.6c0-15.36,10.24-25.6,25.6-25.6s25.6,10.24,25.6,25.6L384,435.2z"
/>
</g>
</g>
</svg>
</template>

View File

@ -1,77 +0,0 @@
import accounting from 'accounting';
import {ref} from 'vue';
accounting.settings.currency = {
...accounting.settings.currency,
precision: 2,
format: '%v %s',
decimal: ',',
thousand: '.',
};
export default function () {
const dropping = ref(false);
async function read(file) {
return new Promise((resolve) => {
var reader = new FileReader();
reader.onload = function () {
var r = reader.result;
resolve({
content: r.substr(r.search(',') + 1),
name: file.name,
type: file.type,
size: file.size,
});
};
reader.readAsDataURL(file);
});
}
function onDragEnter(e) {
e.preventDefault();
e.stopPropagation();
dropping.value = true;
}
function onDragOver(e) {
e.preventDefault();
e.stopPropagation();
}
function onDragLeave(e) {
e.preventDefault();
e.stopPropagation();
dropping.value = false;
}
function processDrop(e, maxFiles) {
e.preventDefault();
e.stopPropagation();
let dt = e.dataTransfer;
let files = [...dt.files].slice(0, maxFiles ? maxFiles : dt.files.length);
let result = [];
for (const f in files) {
if (typeof files[f] === 'object') {
result.push(files[f]);
}
}
dropping.value = false;
return result;
}
function size(file) {
if (file.size < 1000) {
return accounting.formatMoney(file.size, {symbol: 'B'});
}
if (file.size < 1000000) {
return accounting.formatMoney(file.size / 1000, {symbol: 'KB'});
}
if (file.size < 1000000000) {
return accounting.formatMoney(file.size / 1000000, {symbol: 'MB'});
}
if (file.size < 1000000000000) {
return accounting.formatMoney(file.size / 1000000000, {symbol: 'GB'});
}
return '';
}
return {dropping, onDragLeave, onDragOver, onDragEnter, processDrop, read};
}

View File

@ -4,15 +4,9 @@
"type": "library",
"license": "MIT",
"autoload": {
"psr-4": {
"Zoomyboy\\MedialibraryHelper\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Zoomyboy\\MedialibraryHelper\\Tests\\": "tests/",
"Workbench\\App\\": "tests/workbench/app/",
"Database\\Factories\\": "tests/workbench/database/factories/"
"Zoomyboy\\MedialibraryHelper\\": "src/"
}
},
"authors": [
@ -22,31 +16,20 @@
}
],
"require": {
"spatie/laravel-medialibrary": "^11.0",
"laravel/framework": "^11.0",
"spatie/laravel-data": "^4.0",
"pestphp/pest": "^3.0"
"ext-imagick": ">=3.6.0",
"spatie/laravel-medialibrary": "^10.7",
"laravel/framework": "^9.50",
"spatie/laravel-data": "^3.1",
"pestphp/pest": "^1.22"
},
"require-dev": {
"orchestra/testbench": "^9.0",
"illuminate/console": "^11.0"
"phpunit/phpunit": "^9.6",
"orchestra/testbench": "^7.0",
"illuminate/console": "^9.2"
},
"scripts": {
"post-autoload-dump": [
"@clear",
"@prepare",
"@php vendor/bin/testbench package:discover --ansi"
],
"clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
"prepare": "@php vendor/bin/testbench package:discover --ansi",
"build": "@php vendor/bin/testbench workbench:build --ansi",
"serve": [
"Composer\\Config::disableProcessTimeout",
"@build",
"@php vendor/bin/testbench serve"
],
"test": [
"@php vendor/bin/pest"
]
},
"config": {

3883
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +0,0 @@
<?php
return [
'temp_disk' => 'temp',
'middleware' => ['web', 'auth:web'],
];

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
@ -9,6 +9,11 @@
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">./src</directory>
</include>
</coverage>
<php>
<env name="APP_KEY" value="AckfSECXIvnK5r28GVIWUAxmbBSjTsmF"/>
<env name="APP_ENV" value="testing"/>
@ -22,9 +27,4 @@
<env name="SESSION_DRIVER" value="array"/>
<env name="TELESCOPE_ENABLED" value="false"/>
</php>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>

View File

@ -2,7 +2,13 @@
namespace Zoomyboy\MedialibraryHelper;
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
class CollectionExtension
{
public function boot(): void
{
MediaCollection::mixin(new class()
{
public function forceFileName()
{
@ -19,11 +25,6 @@ class CollectionExtension
return fn ($callback) => $this->registerCustomCallback('destroyed', $callback);
}
public function convert()
{
return fn ($callback) => $this->registerCustomCallback('convert', $callback);
}
public function after()
{
return fn ($callback) => $this->registerCustomCallback('after', $callback);
@ -73,25 +74,25 @@ class CollectionExtension
};
}
protected function setDefaultCustomCallbacks()
public function setDefaultCustomCallbacks()
{
return function () {
if (property_exists($this, 'customCallbacks')) {
return;
}
$this->convertTo = null;
$this->customCallbacks = collect([
'forceFileName' => fn ($model, $name) => $name,
'convert' => fn ($extension) => $extension,
'maxWidth' => fn ($size) => null,
'stored' => fn ($event) => true,
'after' => fn ($event) => true,
'destroyed' => fn ($event) => true,
'storing' => fn ($adder, $name) => $adder,
'withDefaultProperties' => fn ($path) => [],
'withDefaultProperties' => fn ($path, $pathinfo) => [],
'withPropertyValidation' => fn ($path) => [],
'withFallback' => fn ($parent) => null,
]);
};
}
});
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace Zoomyboy\MedialibraryHelper;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use Illuminate\Support\Collection;
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
use Spatie\LaravelData\DataCollection;
use Spatie\LaravelData\Lazy;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class DeferredMediaData extends Data
{
public string $collectionName;
public Lazy|string $path;
public static function fromPath(string $path, MediaCollection $collection): self
{
return self::factory()->withoutMagicalCreation()->from([
'path' => Lazy::create(fn () => $path),
'collection_name' => $collection->name,
]);
}
public static function collectionFromPaths(Collection $paths, MediaCollection $collection): Collection
{
return static::collect($paths->map(fn ($path) => static::fromPath($path, $collection)));
}
public function with(): array
{
$file = new MediaFile(Storage::disk(config('media-library.temp_disk'))->path($this->path->resolve()));
return [
'is_deferred' => true,
'original_url' => $this->storage()->url($this->path->resolve()),
'name' => $file->getBasename(),
'size' => $file->getSize(),
'file_name' => $file->getFilename(),
'mime_type' => $file->getMimeType(),
'icon' => Storage::disk('public')->url('filetypes/' . str()->slug($file->getMimeType()) . '.svg'),
];
}
protected function storage(): FilesystemAdapter
{
return Storage::disk(config('media-library.temp_disk'));
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace Zoomyboy\MedialibraryHelper;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
trait DefersUploads
{
public function setDeferredUploads(array $uploads): void
{
if (array_key_exists('collection_name', $uploads)) {
$uploads = [$uploads];
}
foreach ($uploads as $upload) {
$this->runMediaUploadForSingleFile($upload);
}
}
protected function runMediaUploadForSingleFile(array $fileData): void
{
$collection = $this->getMediaCollection($fileData['collection_name']);
$file = new MediaFile($this->storage()->path($fileData['file_name']));
$file->setBasename($collection->runCallback('forceFileName', $this, $file->getBasename()));
$adder = $this->addMediaFromDisk('media-library/' . $fileData['file_name'], config('media-library.temp_disk'))
->usingName($file->getBasename())
->usingFileName($file->getFilename())
->withCustomProperties($collection->runCallback('withDefaultProperties', $file->getFilename()));
tap(
$collection->runCallback('storing', $adder, $file->getFilename())->toMediaCollection($collection->name),
fn ($media) => $collection->runCallback('stored', $media)
);
$collection->runCallback('after', $this);
}
protected function storage(): FilesystemAdapter
{
return Storage::disk(config('media-library.temp_disk'));
}
}

View File

@ -2,20 +2,19 @@
namespace Zoomyboy\MedialibraryHelper;
use Imagick;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Collection;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Spatie\Image\Image;
use Spatie\LaravelData\DataCollection;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\MediaCollections\Exceptions\InvalidBase64Data;
use Spatie\MediaLibrary\MediaCollections\FileAdder;
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Zoomyboy\MedialibraryHelper\Rules\ModelRule;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\File\File;
class MediaController
{
@ -24,83 +23,48 @@ class MediaController
public function store(Request $request)
{
$request->validate([
'parent' => ['required', new ModelRule()],
'name' => 'string',
'model' => ['required', 'string', Rule::in(app('media-library-helpers')->keys())],
]);
if (is_null($request->input('parent.id'))) {
return $this->storeDeferred($request);
}
$model = ModelRule::getModel($request->input('parent'));
$collection = ModelRule::getCollection($request->input('parent'));
$model = $this->validateModel($request);
$collection = $model->getMediaCollection($request->input('collection'));
$isSingle = 1 === $collection->collectionSizeLimit;
$this->authorize('storeMedia', [$model, $collection->name]);
$request->validate($isSingle ? [
'payload' => 'array',
'payload.name' => 'required|string|regex:/\..*$/|max:255',
'payload.*' => '',
'payload.name' => 'required|string|max:255',
'payload.content' => 'required|string',
] : [
'payload' => 'required|array|min:1',
'payload.*' => 'array',
'payload.*.name' => 'required|string|regex:/\..*$/|max:255',
'payload.*.content' => 'required|string',
'payload.*.name' => 'string',
'payload.*.content' => 'string',
]);
$content = $isSingle ? [$request->input('payload')] : $request->input('payload');
$medias = collect($content)->map(function ($c) use ($collection, $model) {
$file = new MediaFile($c['name']);
$file->setBasename($collection->runCallback('forceFileName', $model, $file->getBasename()));
$file->setExtension(MediaFile::extensionFromData($c['content']));
$pathinfo = pathinfo($c['name']);
$basename = $collection->runCallback('forceFileName', $model, $pathinfo['filename']);
$path = $basename . '.' . $pathinfo['extension'];
$file->setExtension($collection->runCallback('convert', $file->getExtension()));
$adder = $this->fileAdderFromData($model, $c['content'], $collection)
->usingName($file->getBasename())
->usingFileName($file->getFilename())
->withCustomProperties($collection->runCallback('withDefaultProperties', $file->getFilename()));
->usingName($basename)
->usingFileName($path)
->withCustomProperties($collection->runCallback('withDefaultProperties', $path, $pathinfo));
return tap(
$collection->runCallback('storing', $adder, $file->getFilename())->toMediaCollection($collection->name),
$collection->runCallback('storing', $adder, $path)->toMediaCollection($collection->name),
fn ($media) => $collection->runCallback('stored', $media)
);
});
$collection->runCallback('after', $model->fresh());
return $isSingle ? MediaData::from($medias->first()) : response(MediaData::collect($medias), 201);
}
protected function storeDeferred(Request $request)
{
$modelName = ModelRule::getModelClassName($request->input('parent'));
$collection = ModelRule::getCollection($request->input('parent'));
$isSingle = 1 === $collection->collectionSizeLimit;
$request->validate($isSingle ? [
'payload' => 'array',
'payload.name' => 'required|string|regex:/\..*$/|max:255',
'payload.content' => 'required|string',
] : [
'payload' => 'required|array|min:1',
'payload.*' => 'array',
'payload.*.name' => 'required|string|regex:/\..*$/|max:255',
'payload.*.content' => 'required|string',
]);
$content = $isSingle ? [$request->input('payload')] : $request->input('payload');
$tempPaths = collect($content)->map(function ($c) use ($collection) {
$file = new MediaFile($c['name']);
$file->setExtension(MediaFile::extensionFromData($c['content']));
$file->setExtension($collection->runCallback('convert', $file->getExtension()));
$tmpFile = $this->storeTemporaryFile($c['content'], $collection);
Storage::disk(config('media-library.temp_disk'))->move($tmpFile, 'media-library/' . $file->getFilename());
return 'media-library/' . $file->getFilename();
});
$this->authorize('storeMedia', [$modelName, null, $collection->name]);
return $isSingle ? DeferredMediaData::fromPath($tempPaths->first(), $collection) : response(DeferredMediaData::collectionFromPaths($tempPaths, $collection), 201);
return $isSingle ? MediaData::from($medias->first()) : MediaData::collection($medias);
}
public function update(Request $request, Media $media): MediaData
@ -118,7 +82,7 @@ class MediaController
return MediaData::from($media);
}
public function index(Request $request, $parentModel, int $parentId, string $collectionName): MediaData|JsonResponse
public function index(Request $request, $parentModel, int $parentId, string $collectionName): MediaData|DataCollection
{
$model = app('media-library-helpers')->get($parentModel);
$model = $model::find($parentId);
@ -134,7 +98,7 @@ class MediaController
return $isSingle
? MediaData::from($model->getFirstMedia($collectionName))
: response()->json(MediaData::collect($model->getMedia($collectionName))->toArray());
: MediaData::collection($model->getMedia($collectionName));
}
public function destroy(Media $media, Request $request): JsonResponse
@ -154,7 +118,27 @@ class MediaController
return property_exists($collection, $callback);
}
private function storeTemporaryFile(string $data, MediaCollection $collection): string
protected function validateModel(Request $request): HasMedia
{
$model = app('media-library-helpers')->get($request->input('model'));
$request->validate([
'collection' => [
'required',
'string',
Rule::in((new $model())->getRegisteredMediaCollections()->pluck('name')),
],
]);
$model = $model::find($request->input('id'));
if (!$model) {
throw ValidationException::withMessages(['model' => 'nicht gefunden']);
}
return $model;
}
protected function fileAdderFromData($model, $data, $collection): FileAdder
{
$maxWidth = $collection->runCallback('maxWidth', 9);
if (str_contains($data, ';base64')) {
@ -174,24 +158,21 @@ class MediaController
throw InvalidBase64Data::create();
}
$tmpFile = 'media-library/' . str()->uuid()->toString() . '.' . $collection->runCallback('convert', MediaFile::extensionFromData($data));
Storage::disk(config('media-library.temp_disk'))->put($tmpFile, $binaryData);
$tmpFile = tempnam(sys_get_temp_dir(), 'media-library');
file_put_contents($tmpFile, $binaryData);
$imagePath = Storage::disk(config('media-library.temp_disk'))->path($tmpFile);
$image = Image::load($imagePath);
if (null !== $maxWidth && 'image/jpeg' === Storage::disk(config('media-library.temp_disk'))->mimeType($tmpFile)) {
$image->width($maxWidth);
$i = (new Imagick());
$i->readImage($tmpFile);
if ($i->getImageFormat() === 'HEIC') {
$i->setFormat('jpg');
$i->writeImage($tmpFile);
}
$image->save();
return $tmpFile;
if (null !== $maxWidth && 'image/jpeg' === mime_content_type($tmpFile)) {
Image::load($tmpFile)->width($maxWidth)->save();
}
protected function fileAdderFromData($model, $data, $collection): FileAdder
{
$tmpFile = $this->storeTemporaryFile($data, $collection);
return $model->addMediaFromDisk($tmpFile, config('media-library.temp_disk'));
return $model->addMedia($tmpFile);
}
}

View File

@ -37,17 +37,14 @@ class MediaData extends Data
public bool $fallback = false;
public bool $isDeferred = false;
public static function fromMedia(Media $media): self
{
$conversions = collect($media->getMediaConversionNames())->flip()->map(
fn ($integer, $conversion) => $media->hasGeneratedConversion($conversion)
$conversions = collect($media->getMediaConversionNames())->flip()->map(fn ($integer, $conversion) => $media->hasGeneratedConversion($conversion)
? ['original_url' => $media->getFullUrl($conversion)]
: null,
);
return self::factory()->withoutMagicalCreation()->from([
return self::withoutMagicalCreationFrom([
...$media->toArray(),
'conversions' => $conversions->toArray(),
]);

View File

@ -1,51 +0,0 @@
<?php
namespace Zoomyboy\MedialibraryHelper;
use Symfony\Component\HttpFoundation\File\File;
class MediaFile
{
public File $file;
public function __construct(string $path)
{
$this->file = new File($path, false);
}
public function getBasename(): string
{
return $this->file->getBasename('.' . $this->file->getExtension());
}
public function setBasename(string $basename): void
{
$newInstance = new self(($this->getPath() ? $this->getPath() . '/' : '') . $basename . '.' . $this->getExtension());
$this->file = $newInstance->file;
}
public function setExtension(string $extension): void
{
$newInstance = new self(($this->getPath() ? $this->getPath() . '/' : '') . $this->getBasename() . '.' . $extension);
$this->file = $newInstance->file;
}
public function __call($method, $arguments)
{
return $this->file->{$method}(...$arguments);
}
public static function extensionFromData(string $data): string
{
$tempFile = sys_get_temp_dir() . '/' . str()->uuid()->toString();
file_put_contents($tempFile, base64_decode($data));
$extension = (new self($tempFile))->guessExtension();
unlink($tempFile);
return $extension;
}
}

View File

@ -31,6 +31,6 @@ class OrderController
$model->getMediaCollection($collectionName)->runCallback('after', $model->fresh());
return MediaData::collect($model->getMedia($collectionName))->toArray();
return MediaData::collection($model->getMedia($collectionName));
}
}

View File

@ -1,89 +0,0 @@
<?php
namespace Zoomyboy\MedialibraryHelper\Rules;
use Illuminate\Contracts\Validation\InvokableRule;
use Spatie\MediaLibrary\HasMedia;
use Illuminate\Validation\Factory;
use Illuminate\Validation\Rule;
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
class ModelRule implements InvokableRule
{
public ?string $collection;
public ?int $id;
public ?string $model;
/**
* @param array{?id: int, ?collection: string, ?model: string} $attribute
*/
public function __invoke($attribute, $value, $fail)
{
app(Factory::class)->make([$attribute => $value], [
"{$attribute}.id" => 'nullable|integer|gt:0',
"{$attribute}.model" => ['required', 'string', Rule::in(app('media-library-helpers')->keys())],
"{$attribute}.collection_name" => 'required|string',
])->validate();
$this->model = data_get($value, 'model');
$this->id = data_get($value, 'id');
$this->collection = data_get($value, 'collection_name');
if (is_null($this->id)) {
$this->validateDeferred($attribute, $value);
return;
}
$model = app('media-library-helpers')->get($this->model);
app(Factory::class)->make([$attribute => $value], [
"{$attribute}.collection_name" => ['required', Rule::in((new $model())->getRegisteredMediaCollections()->pluck('name'))],
"{$attribute}.id" => ['required', 'exists:' . (new $model)->getTable() . ',id'],
])->validate();
}
public function validateDeferred($attribute, $value): void
{
app(Factory::class)->make([$attribute => $value], [
"{$attribute}.model" => ['required', 'string', Rule::in(app('media-library-helpers')->keys())],
"{$attribute}.collection_name" => 'required|string',
])->validate();
$model = app('media-library-helpers')->get($this->model);
app(Factory::class)->make([$attribute => $value], [
"{$attribute}.collection_name" => ['required', Rule::in((new $model())->getRegisteredMediaCollections()->pluck('name'))],
])->validate();
}
/**
* @param array{?id: int, ?collection_name: string, ?model: string} $modelParam
*/
public static function getModel($modelParam): HasMedia
{
$model = static::getModelClassName($modelParam);
return $model::find($modelParam['id']);
}
/**
* @param array{?id: int, ?collection_name: string, ?model: string} $modelParam
* @return class-string<HasMedia>
*/
public static function getModelClassName($modelParam): string
{
return app('media-library-helpers')->get($modelParam['model']);
}
/**
* @param array{?id: int, ?collection_name: string, ?model: string} $modelParam
*/
public static function getCollection($modelParam): MediaCollection
{
$className = static::getModelClassName($modelParam);
return (new $className)->getMediaCollection($modelParam['collection_name']);
}
}

View File

@ -4,7 +4,6 @@ namespace Zoomyboy\MedialibraryHelper;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
class ServiceProvider extends BaseServiceProvider
{
@ -12,8 +11,6 @@ class ServiceProvider extends BaseServiceProvider
{
app()->bind('media-library-helpers', fn () => collect([]));
app()->singleton(CollectionExtension::class, fn () => new CollectionExtension());
$this->mergeConfigFrom(__DIR__ . '/../config/media-library.php', 'media-library');
}
public function boot(): void
@ -26,8 +23,7 @@ class ServiceProvider extends BaseServiceProvider
$router->patch('mediaupload/{media}', [MediaController::class, 'update'])->name('media.update');
});
MediaCollection::mixin(app(CollectionExtension::class));
app(CollectionExtension::class)->boot();
}
/**

View File

@ -1,20 +0,0 @@
providers:
- Spatie\MediaLibrary\MediaLibraryServiceProvider
- Spatie\LaravelData\LaravelDataServiceProvider
- Zoomyboy\MedialibraryHelper\ServiceProvider
migrations:
- tests/workbench/database/migrations
workbench:
start: '/'
install: false
discovers:
web: false
api: false
commands: false
components: false
views: false
build: []
assets: []
sync: []

View File

@ -1,6 +1,6 @@
<?php
namespace Workbench\App\Events;
namespace Zoomyboy\MedialibraryHelper\Tests\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

View File

@ -1,6 +1,6 @@
<?php
namespace Workbench\App\Events;
namespace Zoomyboy\MedialibraryHelper\Tests\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

View File

@ -1,6 +1,6 @@
<?php
namespace Workbench\App\Events;
namespace Zoomyboy\MedialibraryHelper\Tests\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

View File

@ -1,196 +0,0 @@
<?php
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Carbon\Carbon;
use Illuminate\Support\Facades\Storage;
test('it uploads a deferred file to a collection', function () {
$this->auth()->registerModel()->withoutExceptionHandling();
test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => null],
'payload' => ['content' => base64_encode(test()->pdfFile()->getContent()), 'name' => 'beispiel.pdf'],
])
->assertStatus(201)
->assertExactJson([
'is_deferred' => true,
'original_url' => Storage::disk('temp')->url('media-library/beispiel.pdf'),
'name' => 'beispiel',
'collection_name' => 'defaultSingleFile',
'size' => 64576,
'file_name' => 'beispiel.pdf',
'mime_type' => 'application/pdf',
'icon' => url('storage/filetypes/applicationpdf.svg'),
]);
Storage::disk('temp')->assertExists('media-library/beispiel.pdf');
});
test('it forces filename when uploading', function () {
test()->auth()->registerModel()->withoutExceptionHandling();
test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'singleJpegFile', 'id' => null],
'payload' => ['content' => base64_encode(test()->pngFile()->getContent()), 'name' => 'beispiel.png'],
])
->assertStatus(201)
->assertExactJson([
'is_deferred' => true,
'original_url' => Storage::disk('temp')->url('media-library/beispiel.jpg'),
'name' => 'beispiel',
'collection_name' => 'singleJpegFile',
'size' => 490278,
'file_name' => 'beispiel.jpg',
'mime_type' => 'image/jpeg',
'icon' => url('storage/filetypes/imagejpeg.svg'),
]);
Storage::disk('temp')->assertExists('media-library/beispiel.jpg');
Storage::disk('temp')->assertMissing('media-library/beispiel.png');
});
test('it stores a file to media library after deferred upload', function () {
Carbon::setTestNow(Carbon::parse('2023-05-06 06:00:00'));
test()->auth()->registerModel()->withoutExceptionHandling();
Storage::disk('temp')->put('media-library/beispiel.pdf', test()->pdfFile()->getContent());
$post = test()->newPost();
$post->setDeferredUploads([
'file_name' => 'beispiel.pdf',
'collection_name' => 'singleForced',
]);
$media = $post->getMedia('singleForced')->first();
test()->assertNotNull($media);
test()->assertEquals('beispiel 2023-05-06', $media->name);
Storage::disk('temp')->assertMissing('media-library/beispiel.pdf');
});
test('it stores multiple files to media library after deferred upload', function () {
Carbon::setTestNow(Carbon::parse('2023-05-06 06:00:00'));
test()->auth()->registerModel()->withoutExceptionHandling();
Storage::disk('temp')->put('media-library/beispiel.pdf', test()->pdfFile()->getContent());
Storage::disk('temp')->put('media-library/beispiel2.pdf', test()->pdfFile()->getContent());
$post = test()->newPost();
$post->setDeferredUploads([
[
'file_name' => 'beispiel.pdf',
'collection_name' => 'multipleForced',
],
[
'file_name' => 'beispiel2.pdf',
'collection_name' => 'multipleForced',
]
]);
$medias = $post->getMedia('multipleForced');
test()->assertCount(2, $medias);
test()->assertEquals('beispiel 2023-05-06', $medias->get(0)->name);
test()->assertEquals('beispiel2 2023-05-06', $medias->get(1)->name);
Storage::disk('temp')->assertMissing('media-library/beispiel.pdf');
Storage::disk('temp')->assertMissing('media-library/beispiel2.pdf');
});
test('it uploads multiple files', function () {
test()->auth()->registerModel()->withoutExceptionHandling();
$content = base64_encode(test()->pdfFile()->getContent());
$payload = [
'parent' => ['model' => 'post', 'collection_name' => 'multipleForced', 'id' => null],
'payload' => [
['content' => $content, 'name' => 'beispiel.pdf'],
['content' => $content, 'name' => 'beispiel2.pdf'],
]
];
test()->postJson('/mediaupload', $payload)
->assertStatus(201)
->assertJson([
[
'is_deferred' => true,
'original_url' => Storage::disk('temp')->url('media-library/beispiel.pdf'),
'name' => 'beispiel',
'collection_name' => 'multipleForced',
'size' => 64576,
'file_name' => 'beispiel.pdf',
'mime_type' => 'application/pdf',
],
[
'is_deferred' => true,
'original_url' => Storage::disk('temp')->url('media-library/beispiel2.pdf'),
'name' => 'beispiel2',
'collection_name' => 'multipleForced',
'size' => 64576,
'file_name' => 'beispiel2.pdf',
'mime_type' => 'application/pdf',
]
]);
Storage::disk('temp')->assertExists('media-library/beispiel.pdf');
Storage::disk('temp')->assertExists('media-library/beispiel2.pdf');
});
test('it reduces file size', function () {
test()->auth()->registerModel()->withoutExceptionHandling();
test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => null],
'payload' => [
'content' => base64_encode(test()->jpgFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
]);
$size = getimagesize(Storage::disk('temp')->path('media-library/beispiel bild.jpg'));
test()->assertEquals(250, $size[0]);
});
test('it handles authorization with collection', function () {
test()->auth(['storeMedia' => ['collection' => 'rtrt']])->registerModel();
$content = base64_encode(test()->pdfFile()->getContent());
test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => null],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
],
])->assertStatus(403);
});
test('it handles authorization with collection correctly', function () {
test()->auth(['storeMedia' => ['collection' => 'defaultSingleFile']])->registerModel();
$content = base64_encode(test()->pdfFile()->getContent());
test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => null],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
],
])->assertStatus(201);
});
test('it needs a collection', function ($key, $value) {
test()->auth()->registerModel();
$content = base64_encode(test()->pdfFile()->getContent());
$payload = [
'parent' => ['model' => 'post', 'collection' => '', 'id' => null],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
],
];
data_set($payload, $key, $value);
test()->postJson('/mediaupload', $payload)->assertJsonValidationErrors($key);
})->with(function () {
yield ['parent.collection_name', ''];
yield ['parent.collection_name', -1];
yield ['parent.collection_name', 'missingcollection'];
yield ['parent.model', 'lalala'];
yield ['parent.model', -1];
yield ['parent.model', ''];
});

View File

@ -3,8 +3,8 @@
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Illuminate\Support\Facades\Event;
use Workbench\App\Events\MediaChange;
use Workbench\App\Events\MediaDestroyed;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaChange;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaDestroyed;
test('it deletes multiple media', function () {
$this->auth()->registerModel()->withoutExceptionHandling();

View File

@ -7,7 +7,6 @@ use Spatie\MediaLibrary\MediaCollections\Models\Media;
test('it gets all medias', function () {
$this->auth()->registerModel();
$this->withoutExceptionHandling();
$post = $this->newPost();
$firstMedia = $post->addMedia($this->pdfFile()->getPathname())->withCustomProperties(['test' => 'old'])->preservingOriginal()->toMediaCollection('images');
$secondMedia = $post->addMedia($this->pdfFile()->getPathname())->withCustomProperties(['test' => 'old'])->preservingOriginal()->toMediaCollection('images');

View File

@ -1,24 +0,0 @@
<?php
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Illuminate\Foundation\Console\VendorPublishCommand;
use Illuminate\Support\Facades\Artisan;
use Spatie\MediaLibrary\MediaLibraryServiceProvider;
afterEach(function () {
@unlink(config_path('media-library.php'));
});
test('modifies config file', function () {
Artisan::call(VendorPublishCommand::class, ['--provider' => MediaLibraryServiceProvider::class, '--tag' => 'config']);
$configContents = file_get_contents(config_path('media-library.php'));
$configContents = preg_replace('/\'image_driver\' => env.*/', '\'image_driver\' => "lala",', $configContents);
file_put_contents(config_path('media-library.php'), $configContents);
$this->tearDownTheTestEnvironment();
$this->setUpTheTestEnvironment();
$this->assertEquals('lala', config('media-library.image_driver'));
$this->assertEquals('temp', config('media-library.temp_disk'));
});

View File

@ -3,7 +3,7 @@
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Illuminate\Support\Facades\Event;
use Workbench\App\Events\MediaChange;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaChange;
test('it can reorder media', function () {
Event::fake();

View File

@ -3,7 +3,7 @@
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Illuminate\Support\Facades\Event;
use Workbench\App\Events\MediaChange;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaChange;
test('it updates a single files properties', function () {
Event::fake();

View File

@ -4,65 +4,62 @@ namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Carbon\Carbon;
use Illuminate\Support\Facades\Event;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Symfony\Component\Mime\MimeTypes;
use Workbench\App\Events\MediaChange;
use Workbench\App\Events\MediaStored;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaChange;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaStored;
test('it uploads a single file to a single file collection', function () {
test()->auth()->registerModel()->withoutExceptionHandling();
$post = test()->newPost();
$content = base64_encode(test()->pdfFile()->getContent());
$this->auth()->registerModel();
$post = $this->newPost();
$content = base64_encode($this->pdfFile()->getContent());
$response = test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
'payload' => [
'content' => $content,
'name' => 'beispiel bild.pdf',
'name' => 'beispiel bild.jpg',
],
]);
$response->assertStatus(201);
test()->assertCount(1, $post->getMedia('defaultSingleFile'));
$this->assertCount(1, $post->getMedia('defaultSingleFile'));
$media = $post->getFirstMedia('defaultSingleFile');
$response->assertJsonPath('id', $media->id);
$response->assertJsonPath('original_url', $media->getFullUrl());
$response->assertJsonPath('size', 3028);
$response->assertJsonPath('name', 'beispiel bild');
$response->assertJsonPath('collection_name', 'defaultSingleFile');
$response->assertJsonPath('file_name', 'beispiel-bild.pdf');
$response->assertJsonPath('is_deferred', false);
$response->assertJsonPath('file_name', 'beispiel-bild.jpg');
$response->assertJsonMissingPath('model_type');
$response->assertJsonMissingPath('model_id');
});
test('it changes format', function () {
test()->auth()->registerModel();
$post = test()->newPost();
$content = base64_encode(test()->pngFile()->getContent());
test('it uploads heig image', function () {
$this->auth()->registerModel();
$post = $this->newPost();
$content = base64_encode($this->getFile('heic.jpg', 'heic.jpg')->getContent());
$response = test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'singleJpegFile', 'id' => $post->id],
$this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'conversionsWithDefault',
'payload' => [
'content' => $content,
'name' => 'beispiel bild.png',
'name' => 'beispiel bild.jpg',
],
]);
$response->assertStatus(201)
->assertJsonPath('size', 490278)
->assertJsonPath('file_name', 'beispiel-bild.jpg')
->assertJsonPath('mime_type', 'image/jpeg');
$this->assertEquals('image/jpeg', MimeTypes::getDefault()->guessMimeType($post->getFirstMedia('singleJpegFile')->getPath()));
])->assertStatus(201);
});
test('it uploads a single image to a single file collection', function () {
test()->auth()->registerModel()->withoutExceptionHandling();
$post = test()->newPost();
$content = base64_encode(test()->jpgFile()->getContent());
$this->auth()->registerModel();
$post = $this->newPost();
$content = base64_encode($this->jpgFile()->getContent());
$response = test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
@ -70,17 +67,19 @@ test('it uploads a single image to a single file collection', function () {
]);
$response->assertStatus(201);
test()->assertCount(1, $post->getMedia('defaultSingleFile'));
$this->assertCount(1, $post->getMedia('defaultSingleFile'));
});
test('it forces a filename for a single collection', function () {
Carbon::setTestNow(Carbon::parse('2023-04-04 00:00:00'));
test()->auth()->registerModel();
$post = test()->newPost();
$content = base64_encode(test()->jpgFile()->getContent());
$this->auth()->registerModel();
$post = $this->newPost();
$content = base64_encode($this->pdfFile()->getContent());
$response = test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'singleForced', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'singleForced',
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
@ -88,18 +87,20 @@ test('it forces a filename for a single collection', function () {
]);
$response->assertStatus(201);
test()->assertEquals('beispiel-bild-2023-04-04.jpg', $post->getFirstMedia('singleForced')->file_name);
$this->assertEquals('beispiel-bild-2023-04-04.jpg', $post->getFirstMedia('singleForced')->file_name);
$response->assertJsonPath('name', 'beispiel bild 2023-04-04');
$response->assertJsonPath('file_name', 'beispiel-bild-2023-04-04.jpg');
});
test('it sets custom title when storing', function () {
test()->auth()->registerModel();
$post = test()->newPost();
$content = base64_encode(test()->pdfFile()->getContent());
$this->auth()->registerModel();
$post = $this->newPost();
$content = base64_encode($this->pdfFile()->getContent());
$response = test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'singleStoringHook', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'singleStoringHook',
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
@ -109,37 +110,41 @@ test('it sets custom title when storing', function () {
$response->assertStatus(201);
$media = $post->getFirstMedia('singleStoringHook');
test()->assertEquals('AAA', $media->getCustomProperty('use'));
test()->assertEquals('beispiel bild', $media->getCustomProperty('ttt'));
$this->assertEquals('AAA', $media->getCustomProperty('use'));
$this->assertEquals('beispiel bild', $media->getCustomProperty('ttt'));
});
test('it sets custom properties from properties method', function () {
test()->auth()->registerModel();
$post = test()->newPost();
$content = base64_encode(test()->pdfFile()->getContent());
$this->auth()->registerModel();
$post = $this->newPost();
$content = base64_encode($this->pdfFile()->getContent());
$response = test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'multipleProperties', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'multipleProperties',
'payload' => [
'content' => $content,
'name' => 'beispiel bild.pdf',
'name' => 'beispiel bild.jpg',
],
]);
$response->assertStatus(201);
$media = $post->getFirstMedia('multipleProperties');
test()->assertEquals('beispielBild.pdf', $media->getCustomProperty('test'));
$this->assertEquals('beispielBild.jpg', $media->getCustomProperty('test'));
});
test('it forces a filename for multiple collections', function () {
Carbon::setTestNow(Carbon::parse('2023-04-04 00:00:00'));
test()->auth()->registerModel();
$post = test()->newPost();
$content = base64_encode(test()->jpgFile()->getContent());
$this->auth()->registerModel();
$post = $this->newPost();
$content = base64_encode($this->pdfFile()->getContent());
$response = test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'multipleForced', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'multipleForced',
'payload' => [
[
'content' => $content,
@ -149,18 +154,20 @@ test('it forces a filename for multiple collections', function () {
]);
$response->assertStatus(201);
test()->assertEquals('beispiel-bild-2023-04-04.jpg', $post->getFirstMedia('multipleForced')->file_name);
$this->assertEquals('beispiel-bild-2023-04-04.jpg', $post->getFirstMedia('multipleForced')->file_name);
});
test('it throws event when file has been uploaded', function () {
Event::fake();
Carbon::setTestNow(Carbon::parse('2023-04-04 00:00:00'));
test()->auth()->registerModel()->withoutExceptionHandling();
$post = test()->newPost();
$content = base64_encode(test()->pdfFile()->getContent());
$this->auth()->registerModel()->withoutExceptionHandling();
$post = $this->newPost();
$content = base64_encode($this->pdfFile()->getContent());
$response = test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'singleWithEvent', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'singleWithEvent',
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
@ -175,12 +182,14 @@ test('it throws event when file has been uploaded', function () {
test('it throws event when multiple files uploaded', function () {
Event::fake();
Carbon::setTestNow(Carbon::parse('2023-04-04 00:00:00'));
test()->auth()->registerModel()->withoutExceptionHandling();
$post = test()->newPost();
$content = base64_encode(test()->pdfFile()->getContent());
$this->auth()->registerModel()->withoutExceptionHandling();
$post = $this->newPost();
$content = base64_encode($this->pdfFile()->getContent());
$response = test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'multipleFilesWithEvent', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'multipleFilesWithEvent',
'payload' => [
[
'content' => $content,
@ -199,164 +208,120 @@ test('it throws event when multiple files uploaded', function () {
});
test('it uploads multiple files', function () {
test()->auth()->registerModel();
$post = test()->newPost();
$file = test()->pdfFile();
$this->auth()->registerModel();
$post = $this->newPost();
$file = $this->pdfFile();
$post->addMedia($file->getPathname())->preservingOriginal()->toMediaCollection('images');
$response = test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'images', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'images',
'payload' => [
[
'content' => base64_encode($file->getContent()),
'name' => 'aaaa.pdf',
'name' => 'aaaa.jpg',
],
[
'content' => base64_encode($file->getContent()),
'name' => 'beispiel bild.pdf',
'name' => 'beispiel bild.jpg',
],
],
]);
$response->assertStatus(201);
test()->assertCount(3, $post->getMedia('images'));
$this->assertEquals('application/pdf', MimeTypes::getDefault()->guessMimeType($post->getFirstMedia('images')->getPath()));
$this->assertCount(3, $post->getMedia('images'));
$media = $post->getMedia('images')->skip(1)->values();
test()->assertCount(2, $response->json());
$this->assertCount(2, $response->json());
$response->assertJsonPath('0.id', $media->get(0)->id);
$response->assertJsonPath('1.id', $media->get(1)->id);
$response->assertJsonPath('0.original_url', $media->first()->getFullUrl());
$response->assertJsonPath('0.size', 64576);
$response->assertJsonPath('0.size', 3028);
$response->assertJsonPath('0.name', 'aaaa');
$response->assertJsonPath('0.collection_name', 'images');
$response->assertJsonPath('0.file_name', 'aaaa.pdf');
$response->assertJsonPath('0.file_name', 'aaaa.jpg');
$response->assertJsonMissingPath('0.model_type');
$response->assertJsonMissingPath('0.model_id');
});
test('it returns 403 when not authorized', function () {
test()->auth(['storeMedia' => false])->registerModel();
$post = test()->newPost();
$this->auth(['storeMedia' => false])->registerModel();
$post = $this->newPost();
test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
'payload' => [
'content' => base64_encode(test()->pdfFile()->getContent()),
'content' => base64_encode($this->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
])->assertStatus(403);
]);
$response->assertStatus(403);
});
test('it checks for model when running authorization', function () {
$otherPost = test()->newPost();
$post = test()->newPost();
test()->auth(['storeMedia' => ['id' => $post->id, 'collection' => 'defaultSingleFile']])->registerModel();
test('it needs validation for single files', function (array $payload, string $invalidFieldName) {
$this->auth()->registerModel();
$post = $this->newPost();
test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
'payload' => [
'content' => base64_encode(test()->pdfFile()->getContent()),
'content' => base64_encode($this->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
])->assertStatus(201);
test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => $otherPost->id],
'payload' => [
'content' => base64_encode(test()->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
])->assertStatus(403);
});
test('it checks for collection when running authorization', function () {
$post = test()->newPost();
test()->auth(['storeMedia' => ['id' => $post->id, 'collection' => 'defaultSingleFile']])->registerModel();
test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => $post->id],
'payload' => [
'content' => base64_encode(test()->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
])->assertStatus(201);
test()->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection_name' => 'singleWithEvent', 'id' => $post->id],
'payload' => [
'content' => base64_encode(test()->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
])->assertStatus(403);
});
test('it needs validation for single files', function (array $payloadOverwrites, string $invalidFieldName) {
test()->auth()->registerModel();
$post = test()->newPost();
$payload = [
'parent' => ['model' => 'post', 'collection_name' => 'defaultSingleFile', 'id' => $post->id],
'payload' => [
'content' => base64_encode(test()->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
];
foreach ($payloadOverwrites as $key => $value) {
data_set($payload, $key, $value);
}
$response = test()->postJson('/mediaupload', $payload);
...$payload,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors($invalidFieldName);
})->with(function () {
yield [['parent.model' => 'missingmodel'], 'parent.model'];
yield [['parent.id' => -1], 'parent.id'];
yield [['parent.collection_name' => 'missingcollection'], 'parent.collection_name'];
yield [['payload.content' => []], 'payload.content'];
yield [['payload.content' => ['UU']], 'payload.content'];
yield [['payload.content' => null], 'payload.content'];
yield [['payload.content' => ''], 'payload.content'];
yield [['payload.content' => 1], 'payload.content'];
yield [['payload.name' => ''], 'payload.name'];
yield [['payload.name' => ['U']], 'payload.name'];
yield [['payload.name' => 1], 'payload.name'];
yield [['payload.name' => null], 'payload.name'];
yield [['model' => 'missingmodel'], 'model'];
yield [['id' => -1], 'model'];
yield [['collection' => 'missingcollection'], 'collection'];
yield [['payload' => ['name' => 'AAA', 'content' => []]], 'payload.content'];
yield [['payload' => ['name' => 'AAA', 'content' => ['UU']]], 'payload.content'];
yield [['payload' => ['name' => 'AAA', 'content' => null]], 'payload.content'];
yield [['payload' => ['name' => 'AAA', 'content' => '']], 'payload.content'];
yield [['payload' => ['name' => 'AAA', 'content' => 1]], 'payload.content'];
yield [['payload' => ['name' => '', 'content' => 'aaadfdf']], 'payload.name'];
yield [['payload' => ['name' => ['U'], 'content' => 'aaadfdf']], 'payload.name'];
yield [['payload' => ['name' => 1, 'content' => 'aaadfdf']], 'payload.name'];
yield [['payload' => ['name' => null, 'content' => 'aaadfdf']], 'payload.name'];
yield [['payload' => 'lalal'], 'payload'];
yield [['payload' => 55], 'payload'];
});
test('it needs validation for multiple files', function (array $payloadOverwrites, string $invalidFieldName) {
test()->auth()->registerModel();
$post = test()->newPost();
test('it needs validation for multiple files', function (array $payload, string $invalidFieldName) {
$this->auth()->registerModel();
$post = $this->newPost();
$payload = [
'parent' => ['model' => 'post', 'collection_name' => 'images', 'id' => $post->id],
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'images',
'payload' => [
[
'content' => base64_encode(test()->pdfFile()->getContent()),
'content' => base64_encode($this->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
],
];
foreach ($payloadOverwrites as $key => $value) {
data_set($payload, $key, $value);
}
$response = test()->postJson('/mediaupload', $payload);
...$payload,
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors($invalidFieldName);
})->with(function () {
yield [['parent.model' => 'missingmodel'], 'parent.model'];
yield [['parent.model' => 'post.missingcollection'], 'parent.model'];
yield [['parent.id' => -1], 'parent.id'];
yield [['model' => 'missingmodel'], 'model'];
yield [['id' => -1], 'model'];
yield [['collection' => 'missingcollection'], 'collection'];
yield [['payload' => 'lalal'], 'payload'];
yield [['payload' => []], 'payload'];
yield [['payload' => 1], 'payload'];
yield [['payload' => [['name' => 'AAA', 'content' => []]]], 'payload.0.content'];
yield [['payload' => [['name' => 'AAA', 'content' => ['UU']]]], 'payload.0.content'];
yield [['payload' => [['name' => 'AAA', 'content' => null]]], 'payload.0.content'];

View File

@ -1,6 +1,6 @@
<?php
namespace Workbench\App\Models;
namespace Zoomyboy\MedialibraryHelper\Tests\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Event;
@ -8,22 +8,19 @@ use Illuminate\Support\Str;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Workbench\App\Events\MediaChange;
use Workbench\App\Events\MediaDestroyed;
use Workbench\App\Events\MediaStored;
use Zoomyboy\MedialibraryHelper\DefersUploads;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaChange;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaDestroyed;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaStored;
class Post extends Model implements HasMedia
{
use InteractsWithMedia;
use DefersUploads;
public $guarded = [];
public function registerMediaCollections(): void
{
$this->addMediaCollection('defaultSingleFile')->maxWidth(fn () => 250)->singleFile();
$this->addMediaCollection('singleJpegFile')->convert(fn ($extension) => 'jpg')->singleFile();
$this->addMediaCollection('conversionsWithDefault')
->singleFile()
@ -59,7 +56,7 @@ class Post extends Model implements HasMedia
Event::dispatch(new MediaStored($media));
});
$this->addMediaCollection('multipleProperties')->singleFile()->withDefaultProperties(fn ($path) => [
$this->addMediaCollection('multipleProperties')->singleFile()->withDefaultProperties(fn ($path, $pathinfo) => [
'test' => Str::camel($path),
])->withPropertyValidation(fn ($path) => [
'test' => 'string|max:10',

View File

@ -1,11 +1,7 @@
<?php
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Storage;
use Workbench\App\Models\Post;
use Workbench\App\Policies\PostPolicy;
uses(Zoomyboy\MedialibraryHelper\Tests\TestCase::class)->in('Feature');
uses(Illuminate\Foundation\Testing\RefreshDatabase::class)->in('Feature');
uses()->beforeEach(fn () => Storage::fake('media') && Storage::fake('temp'))->in('Feature');
uses()->beforeEach(fn () => Gate::policy(Post::class, PostPolicy::class))->in('Feature');
uses()->beforeEach(fn () => Storage::fake('media'))->in('Feature');

View File

@ -4,15 +4,30 @@ namespace Zoomyboy\MedialibraryHelper\Tests;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Gate;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase as BaseTestCase;
use Workbench\App\Models\Post;
use Workbench\App\Models\User;
use Spatie\LaravelData\LaravelDataServiceProvider;
use Spatie\MediaLibrary\MediaLibraryServiceProvider;
use Zoomyboy\MedialibraryHelper\ServiceProvider;
use Zoomyboy\MedialibraryHelper\Tests\Models\Post;
class TestCase extends BaseTestCase
{
/**
* Define database migrations.
*/
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__ . '/migrations');
}
use WithWorkbench;
protected function getPackageProviders($app): array
{
return [
ServiceProvider::class,
MediaLibraryServiceProvider::class,
LaravelDataServiceProvider::class,
];
}
/**
* Generate a pdf file with a filename and get path.
@ -27,11 +42,6 @@ class TestCase extends BaseTestCase
return $this->getFile('jpg.jpg', $filename ?: 'jpg.jpg');
}
protected function pngFile(?string $filename = null): File
{
return $this->getFile('png.png', $filename ?: 'png.png');
}
protected function getFile(string $location, string $as): File
{
$path = __DIR__ . '/stubs/' . $location;
@ -55,8 +65,23 @@ class TestCase extends BaseTestCase
protected function auth(array $policies = []): self
{
$this->be(User::factory()->policies($policies)->create());
$policies = [
'storeMedia' => true,
'updateMedia' => true,
'destroyMedia' => true,
'listMedia' => true,
...$policies,
];
foreach ($policies as $ability => $result) {
Gate::define($ability, fn (?string $user, string $collectionName) => $result);
}
return $this;
}
protected function defineEnvironment($app)
{
$app['config']->set('media-library.middleware', ['web']);
}
}

BIN
tests/stubs/heic.jpg Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

View File

@ -1,17 +0,0 @@
<?php
namespace Workbench\App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasFactory;
public $guarded = [];
public $casts = [
'policies' => 'json',
];
}

View File

@ -1,56 +0,0 @@
<?php
namespace Workbench\App\Policies;
use Illuminate\Auth\Access\HandlesAuthorization;
use Spatie\MediaLibrary\HasMedia;
use Workbench\App\Models\User;
class PostPolicy
{
use HandlesAuthorization;
public function listMedia(User $user, HasMedia $model): bool
{
if (is_bool($user->policies['listMedia'])) {
return $user->policies['listMedia'] === true;
}
return data_get($user->policies, 'listMedia.id') === $model->id
&& data_get($user->policies, 'listMedia.collection') === $collection;
}
public function storeMedia(User $user, ?HasMedia $model, ?string $collection = null): bool
{
if (is_bool($user->policies['storeMedia'])) {
return $user->policies['storeMedia'] === true;
}
if (is_null($model)) {
return data_get($user->policies, 'storeMedia.collection') === $collection;
}
return data_get($user->policies, 'storeMedia.id') === $model->id
&& data_get($user->policies, 'storeMedia.collection') === $collection;
}
public function updateMedia(User $user, HasMedia $model, string $collection): bool
{
if (is_bool($user->policies['updateMedia'])) {
return $user->policies['updateMedia'] === true;
}
return data_get($user->policies, 'updateMedia.id') === $model->id
&& data_get($user->policies, 'updateMedia.collection') === $collection;
}
public function destroyMedia(User $user, HasMedia $model, string $collection): bool
{
if (is_bool($user->policies['destroyMedia'])) {
return $user->policies['destroyMedia'] === true;
}
return data_get($user->policies, 'destroyMedia.id') === $model->id
&& data_get($user->policies, 'destroyMedia.collection') === $collection;
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace Database\Factories\Models;
use Workbench\App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
protected $model = User::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'email' => $this->faker->safeEmail,
'password' => Hash::make('password'),
'name' => $this->faker->firstName,
'policies' => [],
];
}
public function policies(array $policies): self
{
return $this->state(['policies' => [
'storeMedia' => true,
'updateMedia' => true,
'destroyMedia' => true,
'listMedia' => true,
...$policies,
]]);
}
}

View File

@ -1,20 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('email');
$table->string('name');
$table->string('password');
$table->json('policies');
$table->timestamps();
});
}
};