WIP: Implement uploads

This commit is contained in:
Björn Fromme
2023-09-18 17:56:33 +02:00
parent ce97017ac4
commit d6687c3ca4
51 changed files with 1123 additions and 168 deletions
+4
View File
@@ -25,3 +25,7 @@
npm-debug.log
yarn-error.log
###< symfony/webpack-encore-bundle ###
###> liip/imagine-bundle ###
/public/media/cache/
###< liip/imagine-bundle ###
+7 -2
View File
@@ -24,8 +24,13 @@ export default class extends Controller {
})
this.buttonTargets.forEach((element, index) => {
if (this.hasButtonActiveClass) {
element.classList.toggle(this.buttonActiveClass, activeTab === index)
if (false === this.hasButtonActiveClass) {
return
}
if (activeTab === index) {
element.classList.add(...this.buttonActiveClasses)
} else {
element.classList.remove(...this.buttonActiveClasses)
}
})
}
@@ -0,0 +1,63 @@
import { Controller } from '@hotwired/stimulus'
import { useFetch } from '../mixins/use_fetch'
import Dropzone from 'dropzone'
import 'dropzone/dist/dropzone.css'
Dropzone.autoDiscover = false
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = [
'dropzone',
]
static values = {
endpointUpload: String,
endpointDelete: String,
maxFiles: Number,
maxFilesize: Number,
acceptedFiles: String,
chunking: Boolean,
params: Object,
}
connect () {
useFetch(this)
const dropzone = new Dropzone(this.dropzoneTarget, {
url: this.endpointUploadValue,
withCredentials: true,
maxFiles: this.maxFilesValue,
maxFilesize: this.maxFilesizeValue,
acceptedFiles: this.acceptedFilesValue,
chunking: this.chunkingValue,
chunkSize: 10000000,
parallelUploads: 3,
retryChunks: true,
disablePreviews: false,
createImageThumbnails: true,
autoProcessQueue: true,
addRemoveLinks: true,
})
dropzone.on('sending', (file, xhr, formData) => {
if (this.hasParamsValue) {
let keys = Object.keys(this.paramsValue)
keys.forEach(key => {
formData.append(key, this.paramsValue[key])
})
}
formData.append('uuid', file.upload.uuid)
formData.append('originalFilename', file.name)
window.dispatchEvent(new CustomEvent('dropzone:uploading'))
})
dropzone.on('queuecomplete', () => {
window.dispatchEvent(new CustomEvent('dropzone:complete'))
})
dropzone.on('removedfile', (file) => {
fetch(`${this.endpointDeleteValue}?uuid=${file.upload.uuid}`, { method: 'delete', credentials: 'include' })
})
}
}
-152
View File
@@ -1,152 +0,0 @@
import { Controller } from '@hotwired/stimulus'
import Dropzone from 'dropzone'
Dropzone.autoDiscover = false
/* stimulusFetch: 'lazy' */
export default class extends Controller {
static targets = [
'dropzone',
'queue',
'previewTemplate',
'fieldsTemplate',
'fields',
'error',
'errorMessage',
'upload',
]
static values = {
endpoint: String,
maxFiles: Number,
maxFilesize: Number,
acceptedFiles: String,
chunking: Boolean,
language: Object,
params: Object,
uploads: Array,
error: String,
formName: String,
}
initialize() {
this.fieldsTemplate = this.fieldsTemplateTarget.innerHTML
}
connect () {
const dropzone = new Dropzone(this.dropzoneTarget, {
url: this.endpointValue,
withCredentials: true,
maxFiles: this.maxFilesValue,
maxFilesize: this.maxFilesizeValue,
acceptedFiles: this.acceptedFilesValue,
chunking: this.chunkingValue,
chunkSize: 10000000,
parallelUploads: 3,
retryChunks: true,
previewsContainer: this.queueTarget,
previewTemplate: this.previewTemplateTarget.innerHTML,
createImageThumbnails: true,
dictFileTooBig: this.languageValue.fileTooBig,
dictInvalidFileType: this.languageValue.invalidFileType,
dictMaxFilesExceeded: this.languageValue.maxFilesExceeded,
})
dropzone.on('addedfile', file => {
if (this.maxFilesValue > this.uploadsValue.length) {
this._removeFile(file.id)
return
}
this.errorValue = ''
const uploads = this.uploadsValue;
uploads.pop()
this.uploadsValue = uploads
})
dropzone.on('removedfile', file => {
this._removeFile(file.upload.uuid)
})
dropzone.on('maxfilesexceeded', file => {
this.errorValue = dropzone.options.dictMaxFilesExceeded
dropzone.removeFile(file);
})
dropzone.on('error', (file, errorMessage) => {
this.errorValue = errorMessage;
dropzone.removeFile(file)
})
dropzone.on('dragenter', () => {
this.errorValue = ''
})
dropzone.on('success', file => {
const response = JSON.parse(file.xhr.response)
const uploads = this.uploadsValue
uploads.push({
filename: response.filename,
originalFilename: response.originalFilename,
uploaderType: response.uploaderType,
size: response.size,
mimeType: response.mimeType,
fileId: file.upload.uuid
})
this.uploadsValue = uploads
window.dispatchEvent(new CustomEvent('dropzone', {
detail: 'success'
}))
})
dropzone.on('sending', () => {
window.dispatchEvent(new CustomEvent('dropzone', {
detail: 'uploading'
}))
})
}
errorValueChanged () {
if ('' === this.errorValue) {
this.errorTarget.classList.add('hidden')
} else {
this.errorMessageTarget.innerText = this.errorValue
this.errorTarget.classList.remove('hidden')
}
}
uploadsValueChanged (uploads) {
this.fieldsTarget.innerHTML = ''
uploads.forEach((upload, index) => {
const field = this.fieldsTemplate
.replace(/__formName__/g, this.formNameValue)
.replace(/__index__/g, index)
.replace(/__fileId__/g, upload.fileId)
.replace(/__originalFilename__/g, upload.originalFilename)
.replace(/__filename__/g, upload.filename)
.replace(/__mimeType__/g, upload.mimeType)
.replace(/__size__/g, upload.size)
this.fieldsTarget.insertAdjacentHTML('beforeend', field)
})
}
removeFile ({ params }) {
const fileId = params.fileid
this._removeFile(fileId)
}
_removeFile(fileId) {
this.uploadsValue = this.uploadsValue.filter(upload => {
return upload.fileId !== fileId
})
}
removeUpload (event) {
event.preventDefault()
this.uploadTargets.forEach(element => {
if (element.contains(event.target)) {
element.remove()
}
})
}
}
+5 -1
View File
@@ -1,7 +1,11 @@
.btn {
@apply inline-flex justify-center rounded-lg text-sm uppercase font-semibold py-2.5 px-4 bg-primary text-white hover:bg-primary/80 w-full;
@apply inline-flex justify-center rounded-lg text-sm uppercase font-semibold py-2.5 px-4 bg-primary text-white hover:bg-primary/80 w-full shadow-md;
}
.btn--secondary {
@apply bg-secondary text-gray-800 hover:bg-secondary/80;
}
.btn--active {
@apply shadow-none;
}
+3
View File
@@ -12,7 +12,9 @@
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.15",
"knplabs/knp-menu-bundle": "^3.2",
"liip/imagine-bundle": "^2.11",
"nesbot/carbon": "^2.70",
"oneup/uploader-bundle": "^4.0",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.22",
"symfony/apache-pack": "^1.0",
@@ -46,6 +48,7 @@
"symfony/webpack-encore-bundle": "^2.0",
"symfony/yaml": "6.3.*",
"twig/extra-bundle": "^2.12|^3.0",
"twig/html-extra": "^3.7",
"twig/twig": "^2.12|^3.0"
},
"config": {
Generated
+389 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c7bd8e1f662fa638390820a36f96a7fd",
"content-hash": "2b10ee7954ba92748752ca9e16b1a17a",
"packages": [
{
"name": "doctrine/cache",
@@ -1386,6 +1386,68 @@
],
"time": "2023-01-14T14:17:03+00:00"
},
{
"name": "imagine/imagine",
"version": "1.3.5",
"source": {
"type": "git",
"url": "https://github.com/php-imagine/Imagine.git",
"reference": "7151d553edec4dc2bbac60419f7a74ff34700e7f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-imagine/Imagine/zipball/7151d553edec4dc2bbac60419f7a74ff34700e7f",
"reference": "7151d553edec4dc2bbac60419f7a74ff34700e7f",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4 || ^9.3"
},
"suggest": {
"ext-exif": "to read EXIF metadata",
"ext-gd": "to use the GD implementation",
"ext-gmagick": "to use the Gmagick implementation",
"ext-imagick": "to use the Imagick implementation"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-develop": "1.x-dev"
}
},
"autoload": {
"psr-4": {
"Imagine\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bulat Shakirzyanov",
"email": "[email protected]",
"homepage": "http://avalanche123.com"
}
],
"description": "Image processing for PHP 5.3",
"homepage": "http://imagine.readthedocs.org/",
"keywords": [
"drawing",
"graphics",
"image manipulation",
"image processing"
],
"support": {
"issues": "https://github.com/php-imagine/Imagine/issues",
"source": "https://github.com/php-imagine/Imagine/tree/1.3.5"
},
"time": "2023-06-07T14:49:52+00:00"
},
{
"name": "knplabs/knp-menu",
"version": "v3.4.0",
@@ -1523,6 +1585,109 @@
},
"time": "2021-10-24T07:53:34+00:00"
},
{
"name": "liip/imagine-bundle",
"version": "2.11.0",
"source": {
"type": "git",
"url": "https://github.com/liip/LiipImagineBundle.git",
"reference": "2e943e6be4309ec90fcff90227053898203c1c8e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/liip/LiipImagineBundle/zipball/2e943e6be4309ec90fcff90227053898203c1c8e",
"reference": "2e943e6be4309ec90fcff90227053898203c1c8e",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"imagine/imagine": "^1.3.2",
"php": "^7.1|^8.0",
"symfony/filesystem": "^3.4|^4.4|^5.3|^6.0",
"symfony/finder": "^3.4|^4.4|^5.3|^6.0",
"symfony/framework-bundle": "^3.4.23|^4.4|^5.3|^6.0",
"symfony/mime": "^4.4|^5.3|^6.0",
"symfony/options-resolver": "^3.4|^4.4|^5.3|^6.0",
"symfony/process": "^3.4|^4.4|^5.3|^6.0",
"twig/twig": "^1.44|^2.9|^3.0"
},
"require-dev": {
"amazonwebservices/aws-sdk-for-php": "^1.0",
"aws/aws-sdk-php": "^2.4",
"doctrine/cache": "^1.11|^2.0",
"doctrine/persistence": "^1.3|^2.0",
"enqueue/enqueue-bundle": "^0.9|^0.10",
"ext-gd": "*",
"league/flysystem": "^1.0|^2.0|^3.0",
"phpstan/phpstan": "^0.12.64",
"psr/cache": "^1.0|^2.0|^3.0",
"psr/log": "^1.0",
"symfony/browser-kit": "^3.4|^4.4|^5.3|^6.0",
"symfony/cache": "^3.4|^4.4|^5.3|^6.0",
"symfony/console": "^3.4|^4.4|^5.3|^6.0",
"symfony/dependency-injection": "^3.4|^4.4|^5.3|^6.0",
"symfony/form": "^3.4|^4.4|^5.3|^6.0",
"symfony/messenger": "^4.4|^5.3|^6.0",
"symfony/phpunit-bridge": "^5.3",
"symfony/templating": "^3.4|^4.4|^5.3|^6.0",
"symfony/validator": "^3.4|^4.4|^5.3|^6.0",
"symfony/yaml": "^3.4|^4.4|^5.3|^6.0"
},
"suggest": {
"alcaeus/mongo-php-adapter": "required for mongodb components",
"amazonwebservices/aws-sdk-for-php": "required to use AWS version 1 cache resolver",
"aws/aws-sdk-php": "required to use AWS version 2/3 cache resolver",
"doctrine/mongodb-odm": "required to use mongodb-backed doctrine components",
"enqueue/enqueue-bundle": "^0.9 add if you like to process images in background",
"ext-exif": "required to read EXIF metadata from images",
"ext-gd": "required to use gd driver",
"ext-gmagick": "required to use gmagick driver",
"ext-imagick": "required to use imagick driver",
"ext-mongodb": "required for mongodb components",
"league/flysystem": "required to use FlySystem data loader or cache resolver",
"monolog/monolog": "A psr/log compatible logger is required to enable logging",
"rokka/imagine-vips": "required to use 'vips' driver",
"symfony/messenger": "If you like to process images in background",
"symfony/templating": "required to use deprecated Templating component instead of Twig"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Liip\\ImagineBundle\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Liip and other contributors",
"homepage": "https://github.com/liip/LiipImagineBundle/contributors"
}
],
"description": "This bundle provides an image manipulation abstraction toolkit for Symfony-based projects.",
"homepage": "http://liip.ch",
"keywords": [
"bundle",
"image",
"imagine",
"liip",
"manipulation",
"photos",
"pictures",
"symfony",
"transformation"
],
"support": {
"issues": "https://github.com/liip/LiipImagineBundle/issues",
"source": "https://github.com/liip/LiipImagineBundle/tree/2.11.0"
},
"time": "2023-05-16T06:13:14+00:00"
},
{
"name": "monolog/monolog",
"version": "3.4.0",
@@ -1730,6 +1895,100 @@
],
"time": "2023-09-07T16:43:50+00:00"
},
{
"name": "oneup/uploader-bundle",
"version": "4.0.1",
"source": {
"type": "git",
"url": "https://github.com/1up-lab/OneupUploaderBundle.git",
"reference": "34d22041d40ff45f87de56cd617a580d48cbdd8c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/1up-lab/OneupUploaderBundle/zipball/34d22041d40ff45f87de56cd617a580d48cbdd8c",
"reference": "34d22041d40ff45f87de56cd617a580d48cbdd8c",
"shasum": ""
},
"require": {
"php": "^7.4 || ^8.0",
"symfony/asset": "^4.4 || ^5.4 || ^6.0",
"symfony/event-dispatcher-contracts": "^1.0 || ^2.0 || ^3.0",
"symfony/finder": "^4.4 || ^5.4 || ^6.0",
"symfony/framework-bundle": "^4.4 || ^5.4 || ^6.0",
"symfony/mime": "^4.4 || ^5.4 || ^6.0",
"symfony/templating": "^4.4 || ^5.4 || ^6.0",
"symfony/translation": "^4.4 || ^5.4 || ^6.0",
"symfony/translation-contracts": "^1.0 || ^2.0 || ^3.0",
"symfony/yaml": "^4.4 || ^5.4 || ^6.0",
"twig/twig": "^2.4 || ^3.0"
},
"require-dev": {
"amazonwebservices/aws-sdk-for-php": "1.5.*",
"doctrine/common": "^2.12 || ^3.0",
"doctrine/doctrine-bundle": "^2.4",
"friendsofphp/php-cs-fixer": "^2.16",
"knplabs/gaufrette": "^0.9",
"m2mtech/flysystem-stream-wrapper": "^1.0",
"oneup/flysystem-bundle": "^4.1",
"phpstan/phpstan": "^0.12.10",
"phpunit/phpunit": "^9.5",
"sensio/framework-extra-bundle": "^5.0 || ^6.0",
"symfony/browser-kit": "^4.4 || ^5.4 || ^6.0",
"symfony/phpunit-bridge": "^5.4",
"symfony/security-bundle": "^4.4 || ^5.4 || ^6.0",
"symfony/var-dumper": "^4.4 || ^5.4 || ^6.0",
"twistor/flysystem-stream-wrapper": "^1.0"
},
"suggest": {
"knplabs/knp-gaufrette-bundle": "0.1.*",
"m2mtech/flysystem-stream-wrapper": "^1.0 (Required when using Flysystem)",
"oneup/flysystem-bundle": "^3.0"
},
"type": "symfony-bundle",
"autoload": {
"psr-4": {
"Oneup\\UploaderBundle\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jim Schmid",
"email": "[email protected]",
"homepage": "https://1up.io",
"role": "Developer"
},
{
"name": "David Greminger",
"email": "[email protected]",
"homepage": "https://1up.io",
"role": "Developer"
}
],
"description": "This Symfony bundle provides a server implementation for handling single and multiple file uploads using either FineUploader, jQuery File Uploader, YUI3 Uploader, Uploadify, FancyUpload, MooUpload, Plupload or Dropzone. Features include chunked uploads, orphanages, Gaufrette and Flysystem support.",
"homepage": "https://1up.io",
"keywords": [
"FancyUpload",
"FineUploader",
"MooUpload",
"Uploadify",
"YUI3 Uploader",
"blueimp",
"dropzone",
"fileupload",
"jQuery File Uploader",
"plupload",
"upload"
],
"support": {
"issues": "https://github.com/1up-lab/OneupUploaderBundle/issues",
"source": "https://github.com/1up-lab/OneupUploaderBundle/tree/4.0.1"
},
"time": "2022-07-25T07:51:47+00:00"
},
{
"name": "phpdocumentor/reflection-common",
"version": "2.2.0",
@@ -6654,6 +6913,71 @@
],
"time": "2023-03-21T21:06:29+00:00"
},
{
"name": "symfony/templating",
"version": "v6.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/templating.git",
"reference": "27d79475d8bcba894235b5472df158e78c6419a3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/templating/zipball/27d79475d8bcba894235b5472df158e78c6419a3",
"reference": "27d79475d8bcba894235b5472df158e78c6419a3",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/polyfill-ctype": "~1.8"
},
"require-dev": {
"psr/log": "^1|^2|^3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Templating\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "[email protected]"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides all the tools needed to build any kind of template system",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/templating/tree/v6.3.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2023-04-22T08:23:52+00:00"
},
{
"name": "symfony/translation",
"version": "v6.3.0",
@@ -7644,6 +7968,70 @@
],
"time": "2023-05-06T11:11:46+00:00"
},
{
"name": "twig/html-extra",
"version": "v3.7.1",
"source": {
"type": "git",
"url": "https://github.com/twigphp/html-extra.git",
"reference": "95ceb36e70fa8d07af08cf5135ecbf5e0bd8f386"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/html-extra/zipball/95ceb36e70fa8d07af08cf5135ecbf5e0bd8f386",
"reference": "95ceb36e70fa8d07af08cf5135ecbf5e0bd8f386",
"shasum": ""
},
"require": {
"php": ">=7.1.3",
"symfony/mime": "^5.4|^6.0",
"twig/twig": "^2.7|^3.0"
},
"require-dev": {
"symfony/phpunit-bridge": "^5.4|^6.3"
},
"type": "library",
"autoload": {
"psr-4": {
"Twig\\Extra\\Html\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "[email protected]",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
}
],
"description": "A Twig extension for HTML",
"homepage": "https://twig.symfony.com",
"keywords": [
"html",
"twig"
],
"support": {
"source": "https://github.com/twigphp/html-extra/tree/v3.7.1"
},
"funding": [
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/twig/twig",
"type": "tidelift"
}
],
"time": "2023-07-29T15:34:56+00:00"
},
{
"name": "twig/twig",
"version": "v3.6.1",
+2
View File
@@ -14,4 +14,6 @@ return [
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
Oneup\UploaderBundle\OneupUploaderBundle::class => ['all' => true],
Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true],
];
+25
View File
@@ -0,0 +1,25 @@
liip_imagine:
driver: "imagick"
resolvers:
default:
web_path: ~
cache: format_extension
loaders:
default:
filesystem:
data_root: '%kernel.project_dir%/uploads/photo'
twig:
mode: lazy
filter_sets:
cache: ~
thumbnail:
format: jpeg
jpeg_quality: 75
filters:
thumbnail: { size: [ 128, 128 ], mode: outbound }
profile:
format: jpeg
jpeg_quality: 75
filters:
thumbnail: { size: [ 128, 128 ], mode: outbound }
+15
View File
@@ -0,0 +1,15 @@
oneup_uploader:
mappings:
photo:
frontend: dropzone
use_orphanage: true
namer: app.upload_namer
storage:
directory: '%kernel.project_dir%/uploads/photo'
chunks:
maxage: 86400
storage:
directory: '%kernel.project_dir%/uploads/temp'
orphanage:
maxage: 3600
directory: '%kernel.project_dir%/uploads/orphanage'
+2
View File
@@ -0,0 +1,2 @@
_liip_imagine:
resource: "@LiipImagineBundle/Resources/config/routing.yaml"
+3
View File
@@ -0,0 +1,3 @@
oneup_uploader:
resource: .
type: uploader
+42
View File
@@ -19,6 +19,14 @@ services:
- '../src/Entity/'
- '../src/Kernel.php'
Liip\ImagineBundle\Imagine\Cache\Resolver\FormatExtensionResolver:
arguments:
- '@liip_imagine.cache.resolver.default'
- '@liip_imagine.filter.configuration'
tags:
- name: liip_imagine.cache.resolver
resolver: format_extension
App\EventListener\BlamableEntitySubscriber:
tags:
- { name: 'doctrine.event_subscriber', connection: 'default' }
@@ -27,6 +35,13 @@ services:
tags:
- { name: 'doctrine.event_subscriber', connection: 'default' }
App\EventListener\LogoutListener:
tags:
- name: kernel.event_listener
event: Symfony\Component\Security\Http\Event\LogoutEvent
method: onLogout
dispatcher: security.event_dispatcher.main
App\BusProNet\ApiClient:
arguments:
$logger: '@monolog.logger.bpn'
@@ -47,3 +62,30 @@ services:
- name: knp_menu.menu_builder
method: createMainMenu
alias: main
App\Service\Upload\UploadHandler:
arguments:
$orphanageManager: '@oneup_uploader.orphanage_manager'
$projectDir: '%kernel.project_dir%'
App\EventListener\UploadSessionListener:
tags:
- name: kernel.event_listener
event: oneup_uploader.post_upload
method: onUpload
App\EventListener\UploadSessionValidationListener:
tags:
- name: kernel.event_listener
event: oneup_uploader.validation
method: onValidate
App\EventListener\DeleteUploadListener:
tags:
- name: doctrine.event_listener
event: preRemove
app.upload_namer:
class: App\Service\Upload\UploadNamer
public: true
+20 -2
View File
@@ -5,8 +5,10 @@ namespace App\Controller\Teamer;
use App\BusProNet\ApiClient;
use App\BusProNet\ApiClientException;
use App\Entity\Teamer;
use App\Entity\Upload;
use App\Entity\User;
use App\Form\TeamerProfileType;
use App\Service\Upload\UploadHandler;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -18,7 +20,8 @@ class ProfileController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly ApiClient $apiClient
private readonly ApiClient $apiClient,
private readonly UploadHandler $uploadHandler
) {
}
@@ -36,10 +39,22 @@ class ProfileController extends AbstractController
$this->entityManager->persist($teamer);
}
$form = $this->createForm(TeamerProfileType::class, $teamer);
$uploadSession = $this->uploadHandler->getUploadSession();
$form = $this->createForm(TeamerProfileType::class, $teamer, ['upload_session' => $uploadSession]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($uploadSession->getCount() > 0) {
if (null !== $existingUpload = $teamer->getPhoto()) {
$this->entityManager->remove($existingUpload);
}
$upload = Upload::fromUploadDto($uploadSession->getUploads()->first(), $user, Upload::TYPE_PHOTO);
$teamer->setPhoto($upload);
$this->uploadHandler->moveUploadSessionFilesFromOrphanage('photo', $uploadSession);
$this->uploadHandler->destroyUploadSession();
}
try {
$bpnPassword = $request->getSession()->get('bpn_password');
$this->apiClient->updateProfile($user, $bpnPassword);
@@ -47,6 +62,8 @@ class ProfileController extends AbstractController
} catch (ApiClientException $e) {
}
$this->entityManager->flush();
$this->addFlash('success', 'Deine Daten wurden aktualisiert');
return $this->redirectToRoute('app_teamer_profile');
@@ -54,6 +71,7 @@ class ProfileController extends AbstractController
return $this->render('teamer/profile.html.twig', [
'form' => $form->createView(),
'teamer' => $teamer,
]);
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\Controller\Upload;
use App\Service\Upload\UploadHandler;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class DeleteController extends AbstractController
{
public function __construct(private readonly UploadHandler $uploadHandler)
{}
#[Route('/upload/delete', name: 'app_upload_delete', methods: ['DELETE'])]
#[IsGranted('ROLE_USER')]
public function index(Request $request): JsonResponse
{
$uuid = $request->get('uuid');
$this->uploadHandler->removeUploadFromSession($uuid);
return $this->json([
'success' => true,
'uuid' => $uuid,
]);
}
}
+18
View File
@@ -4,6 +4,7 @@ namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Model\UploadDto;
use App\Repository\UploadRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
@@ -14,6 +15,8 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface
use BlameableEntity;
use TimestampableEntity;
public const TYPE_PHOTO = 'photo';
public const STATUS_NEW = 'new';
public const STATUS_IN_PROCESS = 'in_process';
public const STATUS_CHECKED = 'checked';
@@ -52,6 +55,21 @@ class Upload implements BlameableEntityInterface, TimestampableEntityInterface
$this->uuid = Uuid::v4();
}
public static function fromUploadDto(UploadDto $uploadDto, User $owner, string $type): static
{
$instance = new static();
$instance
->setFilename($uploadDto->getFilename())
->setOriginalFilename($uploadDto->getOriginalFilename())
->setMimeType($uploadDto->getMimeType())
->setSize($uploadDto->getSize())
->setOwner($owner)
->setType($type)
;
return $instance;
}
public function getId(): ?int
{
return $this->id;
@@ -0,0 +1,32 @@
<?php
namespace App\EventListener;
use App\Entity\Upload;
use App\Service\Upload\UploadHandler;
use Doctrine\ORM\Event\PreRemoveEventArgs;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
class DeleteUploadListener
{
public function __construct(
private readonly UploadHandler $uploadHandler,
private readonly CacheManager $cacheManager
) {
}
public function preRemove(PreRemoveEventArgs $eventArgs): void
{
$object = $eventArgs->getObject();
if (!$object instanceof Upload) {
return;
}
// Remove uploaded original
$this->uploadHandler->removeUploadedFile($object);
// Remove potential variants from public folders
$this->cacheManager->remove($object->getFilename());
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
class LogoutListener
{
public function __construct(private readonly LoggerInterface $authenticationLogger)
{
}
public function onLogout(LogoutEvent $event): void
{
$this->authenticationLogger->info('Logout', [
'user' => $event->getToken()->getUserIdentifier(),
]);
}
}
@@ -0,0 +1,42 @@
<?php
namespace App\EventListener;
use App\Model\UploadDto;
use App\Service\Upload\UploadHandler;
use Oneup\UploaderBundle\Event\PostUploadEvent;
use Symfony\Component\HttpFoundation\File\File;
class UploadSessionListener
{
public function __construct(private readonly UploadHandler $uploadHandler)
{
}
public function onUpload(PostUploadEvent $event): void
{
$request = $event->getRequest();
/** @var File $uploadedFile */
$uploadedFile = $event->getFile();
// Get upload uuid from Dropzone request
$uuid = $request->get('uuid');
// Get original filename from Dropzone request
$originalFileName = $request->get('originalFilename');
$upload = new UploadDto(
$uuid,
$uploadedFile->getFilename(),
$originalFileName,
$uploadedFile->getMimeType(),
$uploadedFile->getSize()
);
$uploadSession = $this->uploadHandler->addUploadToSession($upload);
$response = $event->getResponse();
$response['uploadedTotalSize'] = $uploadSession->getSize();
$response['uploadedTotalFiles'] = $uploadSession->getCount();
}
}
@@ -0,0 +1,33 @@
<?php
namespace App\EventListener;
use App\Model\UploadSessionDto;
use App\Service\Upload\UploadHandler;
use Oneup\UploaderBundle\Event\ValidationEvent;
use Oneup\UploaderBundle\Uploader\Exception\ValidationException;
class UploadSessionValidationListener
{
public function onValidate(ValidationEvent $event): void
{
if ('upload_collection' !== $event->getType()) {
return;
}
$request = $event->getRequest();
$session = $request->getSession();
$uploadSessionId = $request->get(UploadHandler::SESSION_KEY);
if (null === $uploadSessionId || false === $session->has(UploadHandler::SESSION_KEY)) {
throw new ValidationException('Invalid upload session');
}
/** @var UploadSessionDto $uploadSession */
$uploadSession = $session->get(UploadHandler::SESSION_KEY);
if (!$uploadSession instanceof UploadSessionDto || $uploadSession->getUid() !== $uploadSessionId) {
throw new ValidationException('Invalid upload session');
}
}
}
+20 -3
View File
@@ -3,12 +3,16 @@
namespace App\Form;
use App\Entity\Teamer;
use App\Model\UploadSessionDto;
use App\Service\Upload\UploadHandler;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TeamerProfileType extends AbstractType
@@ -77,10 +81,23 @@ class TeamerProfileType extends AbstractType
;
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
/** @var UploadSessionDto $uploadSession */
$uploadSession = $options['upload_session'];
$view->vars['upload_session_params'] = [
UploadHandler::SESSION_KEY => $uploadSession->getUid(),
];
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Teamer::class,
]);
$resolver
->setDefaults([
'data_class' => Teamer::class,
])
->setRequired(['upload_session'])
->setAllowedTypes('upload_session', UploadSessionDto::class)
;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace App\Model;
class UploadDto
{
private string $uuid;
private string $filename;
private string $originalFilename;
private string $mimeType;
private int $size;
public function __construct(
string $uuid,
string $filename,
string $originalFilename,
string $mimeType,
int $size
) {
$this->uuid = $uuid;
$this->filename = $filename;
$this->originalFilename = $originalFilename;
$this->mimeType = $mimeType;
$this->size = $size;
}
public function getUuid(): string
{
return $this->uuid;
}
public function getFilename(): string
{
return $this->filename;
}
public function getOriginalFilename(): string
{
return $this->originalFilename;
}
public function getMimeType(): string
{
return $this->mimeType;
}
public function getSize(): int
{
return $this->size;
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace App\Model;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert;
class UploadSessionDto
{
private string $uid;
#[Assert\Count(min: 1, minMessage: 'Bitte mindestens eine Datei hochladen')]
private Collection $uploads;
public function __construct(string $uid = null)
{
$this->uid = $uid ?? Uuid::v4();
$this->uploads = new ArrayCollection();
}
public function getUid(): string
{
return $this->uid;
}
public function addUpload(UploadDto $upload): static
{
if (false === $this->uploads->contains($upload)) {
$this->uploads[] = $upload;
}
return $this;
}
public function removeUpload(string $uuid): static
{
$uploadToRemove = $this->uploads->filter(function (UploadDto $upload) use ($uuid) {
return $uuid === $upload->getUuid();
})->first();
if (null !== $uploadToRemove) {
$this->uploads->removeElement($uploadToRemove);
}
return $this;
}
public function getUploads(): Collection
{
return $this->uploads;
}
public function flushUploads(): static
{
$this->uploads->clear();
return $this;
}
public function getSize(): int
{
$size = 0;
foreach ($this->uploads as $upload) {
$size += $upload->getSize();
}
return $size;
}
public function getCount(): int
{
return $this->uploads->count();
}
}
+134
View File
@@ -0,0 +1,134 @@
<?php
namespace App\Service\Upload;
use App\Entity\Upload;
use App\Model\UploadDto;
use App\Model\UploadSessionDto;
use Oneup\UploaderBundle\Uploader\Orphanage\OrphanageManager;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\RequestStack;
class UploadHandler
{
public const SESSION_KEY = 'myep_upload_session';
public function __construct(
private readonly RequestStack $requestStack,
private readonly OrphanageManager $orphanageManager,
private readonly string $projectDir
) {
}
public function getUploadSession(): UploadSessionDto
{
$session = $this->requestStack->getSession();
if ($session->has(self::SESSION_KEY) && null !== $session->get(self::SESSION_KEY)) {
/** @var UploadSessionDto $uploadSession */
$uploadSession = $session->get(self::SESSION_KEY);
} else {
$uploadSession = new UploadSessionDto();
$session->set(self::SESSION_KEY, $uploadSession);
}
return $uploadSession;
}
public function destroyUploadSession(): void
{
$this->requestStack->getSession()->remove(self::SESSION_KEY);
}
public function flushUploadSession(UploadSessionDto $uploadSession): void
{
$uploadSession->flushUploads();
$session = $this->requestStack->getSession();
$session->set(self::SESSION_KEY, $uploadSession);
}
public function addUploadToSession(UploadDto $upload): UploadSessionDto
{
$uploadSession = $this->getUploadSession();
$uploadSession->addUpload($upload);
$session = $this->requestStack->getSession();
$session->set(self::SESSION_KEY, $uploadSession);
return $uploadSession;
}
public function removeUploadFromSession(string $filename): UploadSessionDto
{
$uploadSession = $this->getUploadSession();
$uploadSession->removeUpload($filename);
$session = $this->requestStack->getSession();
$session->set(self::SESSION_KEY, $uploadSession);
return $uploadSession;
}
public function moveUploadSessionFilesFromOrphanage(string $uploader, UploadSessionDto $uploadSession): void
{
$uploadSessionFilenames = $this->getUploadSessionFilenames($uploadSession, true);
$this->moveUploads($uploader, $uploadSessionFilenames);
}
public function moveUploads(string $uploader, array $filenames = []): array
{
$manager = $this->orphanageManager->get($uploader);
if (0 === count($filenames)) {
return $manager->uploadFiles();
}
$files = $manager->getFiles();
$files->files()->name($filenames);
return $manager->uploadFiles(iterator_to_array($files));
}
public function removeUploadedFile(Upload $upload): void
{
$filepath = $this->getUploadFilepath($upload);
$filesystem = new Filesystem();
try {
$filesystem->remove($filepath);
} catch (IOException $e) {
}
}
public function getUploadSessionFilenames(UploadSessionDto $uploadSession, bool $basename = false): array
{
$uploadSessionId = $uploadSession->getUid();
return $uploadSession
->getUploads()
->map(function (UploadDto $uploadDto) use ($basename, $uploadSessionId) {
return true === $basename ? $uploadDto->getFilename() : $this->getTempUploadFilepath($uploadDto, $uploadSessionId);
})
->toArray()
;
}
public function getUploadFilepath(Upload $upload): string
{
return sprintf('%s/uploads/%s/%s', $this->projectDir, $upload->getType(), $upload->getFilename());
}
public function getTempUploadFilepath(UploadDto $uploadDto, string $uploadSessionId): string
{
$tempUploadDir = $this->getTempUploadDir($uploadSessionId);
return sprintf('%s/%s', $tempUploadDir, $uploadDto->getFilename());
}
public function getTempUploadDir(string $uploadSessionId): string
{
return sprintf('%s/uploads/temp/%s', $this->projectDir, $uploadSessionId);
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
namespace App\Service\Upload;
use Oneup\UploaderBundle\Uploader\File\FileInterface;
use Oneup\UploaderBundle\Uploader\Naming\NamerInterface;
use Symfony\Component\Uid\Uuid;
class UploadNamer implements NamerInterface
{
public function name(FileInterface $file): string
{
return sprintf('%s.%s', Uuid::v4(), strtolower($file->getExtension()));
}
}
+26
View File
@@ -29,6 +29,32 @@
"knplabs/knp-menu-bundle": {
"version": "v3.2.0"
},
"liip/imagine-bundle": {
"version": "2.11",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "1.8",
"ref": "d1227d002b70d1a1f941d91845fcd7ac7fbfc929"
},
"files": [
"config/packages/liip_imagine.yaml",
"config/routes/liip_imagine.yaml"
]
},
"oneup/uploader-bundle": {
"version": "4.0",
"recipe": {
"repo": "github.com/symfony/recipes-contrib",
"branch": "main",
"version": "2.0",
"ref": "b19678d1535b9fe7cdf4647fd3483455e4fca458"
},
"files": [
"config/packages/oneup_uploader.yaml",
"config/routes/oneup_uploader.yaml"
]
},
"phpunit/phpunit": {
"version": "9.6",
"recipe": {
@@ -0,0 +1,16 @@
<div {{ stimulus_controller('upload-collection', {
'endpointUpload': endpoint_upload,
'endpointDelete': path('app_upload_delete'),
'maxFiles': 1500,
'maxFilesize': 1000,
'acceptedFiles': null,
'chunking': true,
'params': upload_session_params,
'language': {
'fileTooBig': 'uploader.file_too_big'|trans({ '{maxFilesize}': 1000 }),
'invalidFileType': 'uploader.invalid_file_type'|trans,
'maxFilesExceeded': 'uploader.max_files_exceeded'|trans({ '{maxFiles}': 1500 })
}
}) }}>
<div {{ stimulus_target('upload-collection', 'dropzone') }} class="dropzone"></div>
</div>
+3 -3
View File
@@ -16,18 +16,18 @@
{% endif %}
<form action="{{ path('app_security_login') }}" method="post">
<div class="mb-6">
<label for="username" class="block text-sm font-semibold leading-6 text-gray-900">
<label for="username" class="block leading-6 text-gray-900">
E-Mail:
</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" class="mt-2 appearance-none text-slate-900 bg-white rounded-md block w-full px-3 h-10 shadow-sm sm:text-sm focus:outline-none placeholder:text-slate-400 focus:ring-2 focus:ring-primary ring-1 ring-slate-200">
</div>
<div class="mb-6">
<label for="password" class="block text-sm font-semibold leading-6 text-gray-900">
<label for="password" class="block leading-6 text-gray-900">
Passwort:
</label>
<input type="password" id="password" name="_password" class="mt-2 appearance-none text-slate-900 bg-white rounded-md block w-full px-3 h-10 shadow-sm sm:text-sm focus:outline-none placeholder:text-slate-400 focus:ring-2 focus:ring-primary ring-1 ring-slate-200">
</div>
<button type="submit" class="inline-flex justify-center rounded-lg text-sm font-semibold py-2.5 px-4 bg-slate-900 text-white hover:bg-slate-700 w-full">
<button type="submit" class="btn">
Login
</button>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
+1 -1
View File
@@ -14,7 +14,7 @@
{{ form_widget(form.email) }}
{{ form_errors(form.email) }}
</div>
<button type="submit" class="inline-flex justify-center rounded-lg text-sm font-semibold py-2.5 px-4 bg-slate-900 text-white hover:bg-slate-700 w-full">
<button type="submit" class="btn">
Passwort zurückseten
</button>
{{ form_rest(form) }}
+25 -3
View File
@@ -17,7 +17,7 @@
{% block content %}
{{ form_start(form) }}
<div {{ stimulus_controller('tabs', {}, { 'buttonActive': 'btn--secondary' }) }}>
<div {{ stimulus_controller('tabs', {}, { 'buttonActive': 'btn--secondary btn--active' }) }}>
<div class="grid lg:grid-cols-3 gap-y-8 lg:gap-y-0 lg:gap-x-16">
<div class="flex flex-col space-y-2">
@@ -31,7 +31,7 @@
Bankverbindung
</button>
<button type="button" class="btn" {{ stimulus_target('tabs', 'button') }} {{ stimulus_action('tabs', 'select', null, { 'tab': 3 }) }}>
Sonstiges
Sonstiges/Foto
</button>
</div>
<div class="lg:col-span-2">
@@ -94,11 +94,33 @@
<h3 class="text-xl font-bold pb-2">
Sonstiges
</h3>
<div class="flex flex-col space-y-4">
<div class="flex flex-col space-y-4 pb-8">
{{ form_row(form.taxId) }}
{{ form_row(form.healthInsuranceCompany) }}
{{ form_row(form.remarks) }}
</div>
<h3 class="text-xl font-bold pb-2">
Fotoupload
</h3>
<div class="grid md:grid-cols-2 gap-8">
<div>
{% include '_partials/_upload_collection_form.html.twig' with {
'endpoint_upload': path('_uploader_upload_photo'),
'upload_session_params': form.vars.upload_session_params,
} %}
</div>
<div>
{% if teamer.photo %}
<img src="{{ asset(teamer.photo.filename | imagine_filter('profile')) }}"
class="w-full h-auto rounded-full border-2 border-primary"
alt="{{ teamer.firstName }}">
{% else %}
<svg class="w-full h-auto text-gray-300" viewBox="0 0 10 10" fill="currentColor">
<circle r="5" cx="5" cy="5"/>
</svg>
{% endif %}
</div>
</div>
</div>
<div class="max-w-screen-md mx-auto">
Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB