WIP: Implement uploads

This commit is contained in:
Björn Fromme committed 2023-09-18 17:56:33 +02:00
1 parent ce97017ac4
commit d6687c3ca4
51 files changed
+1123 -168

No files matched your search

+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()));
}
}