Sfoglia il codice sorgente

proposition d'architecture pour les files manager

Vincent 2 anni fa
parent
commit
461f70c92f

+ 24 - 1
config/packages/liip_imagine.yaml

@@ -14,8 +14,31 @@ liip_imagine:
     data_loader: stream.storage
 
     filter_sets:
+        no_filter: ~
         crop_filter:
             filters:
                 crop:
                     size: ~
-                    start: ~
+                    start: ~
+        relative_width_crop_filter:
+            filters:
+                crop:
+                    size: ~
+                    start: ~
+                relative_resize:
+                    widen: ~
+        relative_width_filter:
+            filters:
+                relative_resize:
+                    widen: ~
+        relative_height_crop_filter:
+            filters:
+                crop:
+                    size: ~
+                    start: ~
+                relative_resize:
+                    heighten: ~
+        relative_height_filter:
+            filters:
+                relative_resize:
+                    heighten: ~

+ 4 - 0
config/services.yaml

@@ -71,6 +71,8 @@ services:
             tags: [ 'twig.extension' ]
         App\Service\Cron\CronjobInterface:
             tags: [ 'app.cronjob' ]
+        App\Service\File\Storage\FileStorageInterface:
+            tags: [ 'app.storage' ]
 
     App\Service\ServiceIterator\CurrentAccessExtensionIterator:
         - !tagged_iterator app.extensions.access
@@ -84,6 +86,8 @@ services:
         - !tagged_iterator app.mailer.builder
     App\Service\ServiceIterator\CronjobIterator:
         - !tagged_iterator app.cronjob
+    App\Service\ServiceIterator\StorageIterator:
+        - !tagged_iterator app.storage
 
     #########################################
     ##  SERIALIZER Decorates ##

+ 11 - 12
src/Service/Export/BaseExporter.php

@@ -9,8 +9,8 @@ use App\Enum\Core\FileTypeEnum;
 use App\Repository\Access\AccessRepository;
 use App\Repository\Core\FileRepository;
 use App\Service\Export\Model\ExportModelInterface;
-use App\Service\File\FileManager;
-use App\Service\File\Storage\LocalStorage;
+use App\Service\File\Manager\DocumentManager;
+use App\Service\File\Utils\FileUtils;
 use App\Service\ServiceIterator\EncoderIterator;
 use App\Service\Utils\StringsUtils;
 use Doctrine\ORM\EntityManagerInterface;
@@ -30,8 +30,8 @@ abstract class BaseExporter
     protected EncoderIterator $encoderIterator;
     protected EntityManagerInterface $entityManager;
     protected LoggerInterface $logger;
-    private LocalStorage $localStorage;
-    protected FileManager $fileManager;
+    protected DocumentManager $documentManager;
+    protected FileUtils $fileUtils;
 
     #[Required]
     public function setAccessRepository(AccessRepository $accessRepository): void
@@ -49,14 +49,14 @@ abstract class BaseExporter
     public function setEntityManager(EntityManagerInterface $entityManager): void
     { $this->entityManager = $entityManager; }
     #[Required]
-    public function setLocalStorage(LocalStorage $localStorage): void
-    { $this->localStorage = $localStorage; }
-    #[Required]
-    public function setFileManager(FileManager $fileManager): void
-    { $this->fileManager = $fileManager; }
+    public function setDocumentManager(DocumentManager $documentManager): void
+    { $this->documentManager = $documentManager; }
     #[Required]
     public function setLogger(LoggerInterface $logger): void
     { $this->logger = $logger; }
+    #[Required]
+    public function setFileUtils(FileUtils $fileUtils): void
+    { $this->fileUtils = $fileUtils; }
 
     public function support(ExportRequest $exportRequest): bool
     {
@@ -101,8 +101,7 @@ abstract class BaseExporter
             $file = $this->prepareFile($exportRequest, false);
         }
 
-        // TODO: passer par le FileManager plutôt?
-        return $this->localStorage->writeFile($file, $content, $requester);
+        return $this->documentManager->write($file, $content, $requester);
     }
 
     /**
@@ -133,7 +132,7 @@ abstract class BaseExporter
             $requester,
             true,
             'NOBODY',
-            FileManager::getMimeTypeFromExt($exportRequest->getFormat()),
+            $this->fileUtils->getMimeTypeFromExt($exportRequest->getFormat()),
             $flushFile
         );
     }

+ 152 - 0
src/Service/File/Factory/ImageFactory.php

@@ -0,0 +1,152 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\File\Factory;
+
+use Liip\ImagineBundle\Binary\BinaryInterface;
+use Liip\ImagineBundle\Imagine\Filter\FilterManager;
+
+class ImageFactory
+{
+    const RELATIVE_WIDTH_CROP_FILTER = 'relative_width_crop_filter';
+    const RELATIVE_WIDTH_FILTER = 'relative_width_filter';
+    const RELATIVE_HEIGHT_CROP_FILTER = 'relative_height_crop_filter';
+    const RELATIVE_HEIGHT_FILTER = 'relative_height_filter';
+    const CROP_FILTER = 'crop_filter';
+    const NO_FILTER = 'no_filter';
+
+    public function __construct(
+        private FilterManager $filterManager
+){}
+    /**
+     * Permet de créer et retourner une image selon la configuration du $file et les params de resize
+     * @param BinaryInterface $binary
+     * @param $fileConfig
+     * @param $height
+     * @param $width
+     * @return string
+     */
+    public function create(BinaryInterface $binary, $fileConfig, $height, $width): string{
+        $filter = $this->getFilterType(!empty($fileConfig), $height, $width);
+        $filters_options = $this->getFiltersOptions($filter, $fileConfig, $height, $width);
+
+        //Si on a une config, on créer l'image
+        if($filters_options){
+            $filteredBinary = $this->filterManager->applyFilter($binary, $filter, ['filters' => $filters_options]);
+            $content = $filteredBinary->getContent();
+        }else{
+            $content = $binary->getContent();
+        }
+
+        return $content;
+    }
+
+    /**
+     * Définit le type de filtre à appliquer
+     * @param $cropConfigEnabled
+     * @param $params
+     * @return string
+     */
+    private function getFilterType($cropConfigEnabled, $forcedHeight, $forcedWidth){
+        //Si les params sont présents, on souhaite une image avec une largeur ou hauteur spécifique
+        if($forcedHeight || $forcedWidth){
+            //A partir du moment ou l'un des params est > 0
+            if($forcedHeight > 0 || $forcedWidth > 0){
+                //Si la hauteur est nulle OU si la hateur et la largeur sont renseigné (on ne peux pas autoriser les deux
+                //car l'image serait déformée, donc on ne garde que la largeur dans ce dernier cas)
+                if($forcedHeight == 0 || ($forcedHeight > 0 && $forcedWidth > 0)){
+                    $filter = $cropConfigEnabled ? self::RELATIVE_WIDTH_CROP_FILTER : self::RELATIVE_WIDTH_FILTER;
+                }
+                //Si la largeur est nulle
+                else if($forcedWidth == 0) {
+                    $filter = $cropConfigEnabled ? self::RELATIVE_HEIGHT_CROP_FILTER : self::RELATIVE_HEIGHT_FILTER;
+                }
+            }else{
+                //Si les deux params sont <= 0 alors aucun filtre ne doit être appliqué
+                $filter = self::NO_FILTER;
+            }
+        }
+        //Sinon, si la configuration du crop est faite
+        else if($cropConfigEnabled){
+            $filter = self::CROP_FILTER;
+        }else{
+            //Enfin, aucun filtre ne doit être appliqué
+            $filter = self::NO_FILTER;
+        }
+        return $filter;
+    }
+
+    /**
+     * Construit les options des filtres à appliquer
+     * @param $params
+     */
+
+    private function getFiltersOptions($filter, $fileConfig, $forcedHeight, $forcedWidth){
+        switch ($filter){
+            case self::NO_FILTER:
+                return [];
+
+            case self::CROP_FILTER:
+                return $this->getCropFilterOptions($fileConfig);
+
+            case self::RELATIVE_WIDTH_CROP_FILTER:
+                return array_merge($this->getCropFilterOptions($fileConfig), $this->getRelativeWidhtFilterOptions($forcedWidth));
+
+            case self::RELATIVE_HEIGHT_CROP_FILTER:
+                return array_merge($this->getCropFilterOptions($fileConfig), $this->getRelativeHeightFilterOptions($forcedHeight));
+
+            case self::RELATIVE_HEIGHT_FILTER:
+                return $this->getRelativeHeightFilterOptions($forcedHeight);
+
+            case self::RELATIVE_WIDTH_FILTER:
+                return array_merge($this->getRelativeWidhtFilterOptions($forcedWidth));
+        }
+    }
+
+    /**
+     * Définit et retourne le tableau de config servant à cropper
+     * @param string $config : Configuration du File
+     * @return array<string, array<string, array<string, array<int, int>>>> : tableau de configuration
+     * @see ImageUtilsTest::testGetCroppingConfig()
+     */
+    protected function getCropFilterOptions(string $config): array{
+        $crop_filters_options = [];
+        $config = json_decode($config, true);
+
+        //On s'assure que la hauteur ET la largeur soient > 0
+        if($config['width'] > 0 && $config['height'] > 0){
+            $crop_filters_options = array_merge(
+                [
+                    'crop' => array(
+                        'size' => [intval($config['width']), intval($config['height'])],
+                        'start' => [intval($config['x']), intval($config['y'])]
+                    )
+                ]
+            );
+        }
+        return $crop_filters_options;
+    }
+
+    /**
+     * Construit les options pour le filtre gérant la largeur relative
+     * @param $forcedWidth
+     * @return array[]
+     */
+    private function getRelativeWidhtFilterOptions($forcedWidth){
+        return ['relative_resize' => array(
+            'widen' => intval($forcedWidth)
+        )];
+    }
+
+    /**
+     * Construit les options pour le filtre gérant la hauteur relative
+     * @param $forcedHeight
+     * @return array[]
+     */
+    private function getRelativeHeightFilterOptions($forcedHeight){
+        return ['relative_resize' => array(
+            'heighten' => intval($forcedHeight)
+        )];
+    }
+}

+ 0 - 107
src/Service/File/FileManager.php

@@ -1,107 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Service\File;
-
-use ApiPlatform\Api\IriConverterInterface;
-use ApiPlatform\Api\UrlGeneratorInterface;
-use ApiPlatform\Metadata\Get;
-use App\ApiResources\Core\File\DownloadRequest;
-use App\Entity\Core\File;
-use App\Enum\Core\FileHostEnum;
-use App\Service\File\Exception\FileNotFoundException;
-use App\Service\File\Storage\ApiLegacyStorage;
-use App\Service\File\Storage\FileStorageInterface;
-use App\Service\File\Storage\LocalStorage;
-use Mimey\MimeTypes;
-use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
-
-/**
- * Le gestionnaire de fichiers permet d'effectuer de nombreuses opérations sur les fichiers stockés dans les différents
- * FileStorage (lecture, écriture, suppression...)
- */
-class FileManager
-{
-    public function __construct(
-        private LocalStorage $localStorage,
-        private ApiLegacyStorage $apiLegacyStorage,
-        protected IriConverterInterface $iriConverter
-    ) {}
-
-    /**
-     * Retourne le storage dans lequel le fichier demandé est supposé se trouver
-     * // TODO: voir si ce ne serait pas le boulot d'un ServiceIterator, histoire de rester dans le pattern général
-     *
-     * @param File $file
-     * @return FileStorageInterface
-     * @throws FileNotFoundException
-     */
-    public function getStorageFor(File $file): FileStorageInterface
-    {
-        if ($file->getHost() === FileHostEnum::API1()->getValue()) {
-            return $this->apiLegacyStorage;
-        }
-        if ($file->getHost() === FileHostEnum::AP2I()->getValue()) {
-            return $this->localStorage;
-        }
-        throw new FileNotFoundException('File ' . $file->getId() . ' was not found (unknown host: ' . $file->getHost() . ')');
-    }
-
-    /**
-     * Lit le fichier et retourne son contenu
-     *
-     * @param File $file
-     * @return string
-     * @throws FileNotFoundException
-     */
-    public function read(File $file): string
-    {
-        $storage = $this->getStorageFor($file);
-        return $storage->read($file);
-    }
-
-
-    /**
-     * Get the IRI to download this file (eg: /api/download/1)
-     *
-     * @param File $file
-     * @return string
-     */
-    public function getDownloadIri(File $file): string
-    {
-        return $this->iriConverter->getIriFromResource(
-            DownloadRequest::class,
-            UrlGeneratorInterface::ABS_PATH,
-            new Get(),
-            ['fileId' => $file->getId()]
-        );
-    }
-
-    /**
-     * Return the mimetype corresponding to the givent file extension
-     *
-     * @param string $ext
-     * @return string|null
-     */
-    public static function getMimeTypeFromExt(string $ext): ?string
-    {
-        return (new MimeTypes)->getMimeType(ltrim($ext, '.'));
-    }
-
-    /**
-     * Try to guess the mimetype from the filename
-     *
-     * Return null if it did not manage to guess it.
-     *
-     * @param string $filename
-     * @return string|null
-     */
-    public static function guessMimeTypeFromFilename(string $filename): ?string
-    {
-        $ext = pathinfo($filename, PATHINFO_EXTENSION);
-        if (empty($ext)) {
-            return null;
-        }
-        return self::getMimeTypeFromExt($ext);
-    }
-}

+ 53 - 0
src/Service/File/Manager/AbstractFileManager.php

@@ -0,0 +1,53 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\File\Manager;
+
+use ApiPlatform\Api\IriConverterInterface;
+use ApiPlatform\Api\UrlGeneratorInterface;
+use ApiPlatform\Metadata\Get;
+use App\ApiResources\Core\File\DownloadRequest;
+use App\Entity\Core\File;
+use App\Service\File\Exception\FileNotFoundException;
+use App\Service\File\Storage\FileStorageInterface;
+use App\Service\ServiceIterator\StorageIterator;
+
+/**
+ * Le gestionnaire de fichiers permet d'effectuer de nombreuses opérations sur les fichiers stockés dans les différents
+ * FileStorage (lecture, écriture, suppression...)
+ */
+abstract class AbstractFileManager
+{
+    public function __construct(
+        protected IriConverterInterface $iriConverter,
+        protected StorageIterator $storageIterator
+    ) {}
+
+    /**
+     * Retourne le storage dans lequel le fichier demandé est supposé se trouver
+     *
+     * @param File $file
+     * @return FileStorageInterface
+     * @throws FileNotFoundException
+     */
+    public function getStorageFor(File $file): FileStorageInterface
+    {
+        return $this->storageIterator->getStorageFor($file);
+    }
+
+    /**
+     * Get the IRI to download this file (eg: /api/download/1)
+     *
+     * @param File $file
+     * @return string
+     */
+    public function getDownloadIri(File $file): string
+    {
+        return $this->iriConverter->getIriFromResource(
+            DownloadRequest::class,
+            UrlGeneratorInterface::ABS_PATH,
+            new Get(),
+            ['fileId' => $file->getId()]
+        );
+    }
+}

+ 35 - 0
src/Service/File/Manager/DocumentManager.php

@@ -0,0 +1,35 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\File\Manager;
+
+use App\Entity\Core\File;
+use App\Service\File\Exception\FileNotFoundException;
+
+class DocumentManager extends AbstractFileManager
+{
+    /**
+     * Lit le fichier et retourne son contenu
+     *
+     * @param File $file
+     * @return string
+     * @throws FileNotFoundException
+     */
+    public function read(File $file): string
+    {
+        $storage = $this->getStorageFor($file);
+        return $storage->read($file);
+    }
+
+    /**
+     * @param $file
+     * @param $content
+     * @param $requester
+     * @return File
+     * @throws FileNotFoundException
+     */
+    public function write($file, $content, $requester): File{
+        $storage = $this->getStorageFor($file);
+        return $storage->writeFile($file, $content, $requester);
+    }
+}

+ 36 - 0
src/Service/File/Manager/ImageManager.php

@@ -0,0 +1,36 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\File\Manager;
+
+use ApiPlatform\Api\IriConverterInterface;
+use App\Entity\Core\File;
+use App\Service\File\Exception\FileNotFoundException;
+use App\Service\File\Factory\ImageFactory;
+use App\Service\ServiceIterator\StorageIterator;
+
+class ImageManager extends AbstractFileManager
+{
+    public function __construct(
+        IriConverterInterface $iriConverter,
+        StorageIterator $storageIterator,
+        private ImageFactory $imageFactory
+    )
+    {
+        parent::__construct($iriConverter, $storageIterator);
+    }
+
+    /**
+     * Lit le fichier et retourne son contenu
+     *
+     * @param File $file
+     * @return string
+     * @throws FileNotFoundException
+     */
+    public function read(File $file, int $height = 0, int $width = 0): string
+    {
+        $storage = $this->getStorageFor($file);
+        $binary = $storage->getBinaryImage($file);
+        return $this->imageFactory->create($binary, $file->getConfig(), $height, $width);
+    }
+}

+ 57 - 2
src/Service/File/Storage/ApiLegacyStorage.php

@@ -4,14 +4,19 @@ declare(strict_types=1);
 namespace App\Service\File\Storage;
 
 use App\Entity\Core\File;
+use App\Enum\Core\FileHostEnum;
 use App\Service\ApiLegacy\ApiLegacyRequestService;
+use Liip\ImagineBundle\Binary\BinaryInterface;
+use Liip\ImagineBundle\Imagine\Data\DataManager;
 
 /**
  * Read and write files into the Opentalent API v1 storage
  */
 class ApiLegacyStorage implements FileStorageInterface
 {
-    public function __construct(private ApiLegacyRequestService $apiLegacyRequestService)
+    const TMP_FOLDER = 'gaufrette://storage/temp';
+
+    public function __construct(private ApiLegacyRequestService $apiLegacyRequestService, protected DataManager $dataManager,)
     {}
 
     /**
@@ -22,7 +27,57 @@ class ApiLegacyStorage implements FileStorageInterface
      */
     public function read(File $file): string
     {
-        $url = '_internal/secure/files/' . $file->getId();
+        $url = sprintf('_internal/secure/files/%s', $file->getId());
         return $this->apiLegacyRequestService->getContent($url);
     }
+
+    /**
+     * Get the image file and returns its content as a binary
+     *
+     * @param File $file
+     * @return string
+     */
+    public function getBinaryImage(File $file): BinaryInterface
+    {
+        $url = sprintf('_internal/secure/files/%s/0x0', $file->getId());
+        $imageBase64 = $this->apiLegacyRequestService->getContent($url);
+
+        //Afin de pouvoir manipuler l'image d'origine et d'appliquer des filtres, il faut la sauvegarder en local
+        //et récupérer son Binary
+        $uniq = $this->putFileInTmpFolder($imageBase64);
+        $binary = $this->dataManager->find('no_filter', '/temp/' . $uniq);
+        //Une fois le binary récupéré on supprime l'image
+        $this->deleteImageInTmpFolder($uniq);
+
+        return $binary;
+    }
+
+    /**
+     * @param $imageBase64
+     * @return string
+     */
+    public function putFileInTmpFolder($imageBase64){
+        $uniq = uniqid();
+        $path = sprintf('%s/%s', self::TMP_FOLDER, $uniq);
+        file_put_contents($path, $imageBase64);
+        return $uniq;
+    }
+
+    /**
+     * @param $uniq
+     * @return void
+     */
+    public function deleteImageInTmpFolder($uniq){
+        $path = sprintf('%s/%s', self::TMP_FOLDER, $uniq);
+        unlink($path);
+    }
+
+    /**
+     * @param File $file
+     * @return bool
+     */
+    public function support(File $file): bool
+    {
+        return $file->getHost() === FileHostEnum::API1()->getValue();
+    }
 }

+ 3 - 1
src/Service/File/Storage/FileStorageInterface.php

@@ -4,10 +4,12 @@ declare(strict_types=1);
 namespace App\Service\File\Storage;
 
 use App\Entity\Core\File;
+use Liip\ImagineBundle\Binary\BinaryInterface;
 
 interface FileStorageInterface
 {
     public function read(File $file): string;
-
+    public function getBinaryImage(File $file): BinaryInterface;
+    public function support(File $file): bool;
     // TODO : complete with 'exists', 'write', and 'delete'
 }

+ 29 - 9
src/Service/File/Storage/LocalStorage.php

@@ -7,11 +7,11 @@ use App\Entity\Access\Access;
 use App\Entity\Core\File;
 use App\Entity\Organization\Organization;
 use App\Entity\Person\Person;
+use App\Enum\Core\FileHostEnum;
 use App\Enum\Core\FileStatusEnum;
 use App\Enum\Core\FileTypeEnum;
 use App\Repository\Access\AccessRepository;
-use App\Service\File\FileManager;
-use App\Service\File\Utils\ImageUtils;
+use App\Service\File\Utils\FileUtils;
 use App\Service\Utils\Path;
 use App\Service\Utils\Uuid;
 use DateTime;
@@ -19,6 +19,8 @@ use Doctrine\ORM\EntityManagerInterface;
 use Gaufrette\FilesystemInterface;
 use JetBrains\PhpStorm\Pure;
 use Knp\Bundle\GaufretteBundle\FilesystemMap;
+use Liip\ImagineBundle\Binary\BinaryInterface;
+use Liip\ImagineBundle\Imagine\Data\DataManager;
 use RuntimeException;
 
 /**
@@ -37,7 +39,8 @@ class LocalStorage implements FileStorageInterface
         protected FilesystemMap $filesystemMap,
         protected EntityManagerInterface $entityManager,
         protected AccessRepository $accessRepository,
-        protected ImageUtils $imageUtils
+        protected DataManager $dataManager,
+        protected FileUtils $fileUtils
     )
     {
         $this->filesystem = $filesystemMap->get(static::FS_KEY);
@@ -78,11 +81,19 @@ class LocalStorage implements FileStorageInterface
      */
     public function read(File $file): string
     {
-        if (!$this->imageUtils->isImage($file)) {
-            return $this->filesystem->read($file->getSlug());
-        } else {
-            return $this->imageUtils->formatImage($file);
-        }
+        return $this->filesystem->read($file->getSlug());
+    }
+
+    /**
+     * Get the image file and returns its content as a binary
+     *
+     * @param File $file
+     * @return BinaryInterface
+     */
+    public function getBinaryImage(File $file): BinaryInterface
+    {
+        //On récupère l'image via le dataloader d'imagine liip (dataloader stream qui s'appuie sur gaufrette)
+        return $this->dataManager->find('no_filter', $file->getPath());
     }
 
     /**
@@ -120,7 +131,7 @@ class LocalStorage implements FileStorageInterface
             ->setType($type->getValue())
             ->setVisibility($visibility)
             ->setIsTemporaryFile($isTemporary)
-            ->setMimeType($mimeType ?? FileManager::guessMimeTypeFromFilename($filename))
+            ->setMimeType($mimeType ?? $this->fileUtils->guessMimeTypeFromFilename($filename))
             ->setCreateDate(new DateTime())
             ->setCreatedBy($createdBy->getId())
             ->setStatus(FileStatusEnum::PENDING()->getValue());
@@ -321,4 +332,13 @@ class LocalStorage implements FileStorageInterface
 
         return [null, $owner];
     }
+
+    /**
+     * @param File $file
+     * @return bool
+     */
+    public function support(File $file): bool
+    {
+        return $file->getHost() === FileHostEnum::AP2I()->getValue();
+    }
 }

+ 51 - 0
src/Service/File/Utils/FileUtils.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\File\Utils;
+
+use App\Entity\Core\File;
+use Mimey\MimeTypes;
+
+class FileUtils
+{
+    public function __construct(){}
+
+    /**
+     * Return the mimetype corresponding to the givent file extension
+     *
+     * @param string $ext
+     * @return string|null
+     */
+    public function getMimeTypeFromExt(string $ext): ?string
+    {
+        return (new MimeTypes)->getMimeType(ltrim($ext, '.'));
+    }
+
+    /**
+     * Try to guess the mimetype from the filename
+     *
+     * Return null if it did not manage to guess it.
+     *
+     * @param string $filename
+     * @return string|null
+     */
+    public function guessMimeTypeFromFilename(string $filename): ?string
+    {
+        $ext = pathinfo($filename, PATHINFO_EXTENSION);
+        if (empty($ext)) {
+            return null;
+        }
+        return self::getMimeTypeFromExt($ext);
+    }
+
+    /**
+     * Test si le fichier passé en paramètre est une image
+     * @param File $file
+     * @return bool
+     */
+    public function isImage(File $file): bool
+    {
+        return boolval(preg_match('#^image#', $file->getMimeType()));
+    }
+}

+ 0 - 77
src/Service/File/Utils/ImageUtils.php

@@ -1,77 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-namespace App\Service\File\Utils;
-
-use App\Entity\Core\File;
-use Liip\ImagineBundle\Imagine\Data\DataManager;
-use Liip\ImagineBundle\Imagine\Filter\FilterManager;
-use Psr\Log\LoggerInterface;
-
-class ImageUtils
-{
-    public const FILTER_CROP = 'crop_filter';
-
-    public function __construct(
-        private DataManager $dataManager,
-        private FilterManager $filterManager,
-        private LoggerInterface $logger
-){}
-
-    /**
-     * Permet de cropper et retourner une image selon la configuration du $file
-     * @param File $file : File contenant la définition de l'image
-     * @return string : Image en Base64
-     * @see ImageUtilsTest::testFormatImageWithoutConfig()
-     * @see ImageUtilsTest::testFormatImageWithErrorConfig()
-     * @see ImageUtilsTest::testFormatImageWithConfig()
-     */
-    public function formatImage(File $file): string{
-        //On récupère l'image via le dataloader d'imagine liip (dataloader stream qui s'appuie sur gaufrette)
-        $binary = $this->dataManager->find(self::FILTER_CROP, $file->getPath());
-
-        //Si on a une config, on créer l'image
-        if(!empty($file->getConfig())){
-            try{
-                //On applique le filtre crop et on retourne l'image
-                $binary = $this->filterManager->applyFilter($binary, self::FILTER_CROP, $this->getCroppingConfig($file->getConfig()));
-            }catch(\Exception $e){
-                //Si nous avons une erreur, nous retournons l'image sans crop et on log.
-                $this->logger->error('Error while cropping image');
-                $this->logger->error($e->getMessage());
-            }
-        }
-
-        return $binary->getContent();
-    }
-
-    /**
-     * Définit et retourne le tableau de config servant à cropper
-     * @param string $config : Configuration du File
-     * @return array<string, array<string, array<string, array<int, int>>>> : tableau de configuration
-     * @see ImageUtilsTest::testGetCroppingConfig()
-     */
-    protected function getCroppingConfig(string $config): array{
-        $config = json_decode($config, true);
-        return [
-            'filters' => [
-                'crop' => [
-                    'size' => [intval($config['width']), intval($config['height'])],
-                    'start' => [intval($config['x']), intval($config['y'])]
-                ]
-            ]
-        ];
-    }
-
-    /**
-     * Test si le File est une image
-     * @param File $file
-     * @return bool
-     * @see ImageUtilsTest::testIsImage()
-     * @see ImageUtilsTest::testIsNotImage()
-     */
-    public function isImage(File $file): bool{
-        return boolval(preg_match('#^image#', $file->getMimeType()));
-    }
-}

+ 42 - 0
src/Service/ServiceIterator/StorageIterator.php

@@ -0,0 +1,42 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\ServiceIterator;
+
+use App\Entity\Core\File;
+use App\Service\File\Storage\FileStorageInterface;
+use Exception;
+use RuntimeException;
+
+/**
+ * Permet d'itérer sur les services d'export
+ */
+class StorageIterator
+{
+    /**
+     * Pour l'injection des services, voir config/services.yaml, section 'TAG Services'
+     * @param iterable<FileStorageInterface> $storageServices
+     */
+    public function __construct(
+        readonly private iterable $storageServices,
+    ) {}
+
+    /**
+     * Itère sur les services de storage disponibles et
+     * retourne le premier qui supporte ce type de requête.
+     *
+     * @param File $file
+     * @return FileStorageInterface
+     * @throws Exception
+     */
+    public function getStorageFor(File $file): FileStorageInterface
+    {
+        /** @var FileStorageInterface $storageService */
+        foreach ($this->storageServices as $storageService){
+            if($storageService->support($file)) {
+                return $storageService;
+            }
+        }
+        throw new RuntimeException('no storage service found for this File');
+    }
+}

+ 3 - 3
src/Service/Twig/AssetsExtension.php

@@ -5,7 +5,7 @@ namespace App\Service\Twig;
 
 use App\Entity\Core\File;
 use App\Service\File\Exception\FileNotFoundException;
-use App\Service\File\FileManager;
+use App\Service\File\Manager\ImageManager;
 use App\Service\Utils\Path;
 use Twig\Extension\AbstractExtension;
 use Twig\TwigFunction;
@@ -21,7 +21,7 @@ use Twig\TwigFunction;
 class AssetsExtension extends AbstractExtension
 {
     public function __construct(
-        readonly private FileManager $fileManager
+        readonly private ImageManager $imageManager
     )
     {}
 
@@ -61,6 +61,6 @@ class AssetsExtension extends AbstractExtension
      */
     public function toBase64Src(File $file): string
     {
-        return 'data:' . $file->getMimeType() . ';base64,' . base64_encode($this->fileManager->read($file));
+        return 'data:' . $file->getMimeType() . ';base64,' . base64_encode($this->imageManager->read($file));
     }
 }

+ 36 - 7
src/State/Provider/Core/DownloadRequestProvider.php

@@ -6,11 +6,12 @@ namespace App\State\Provider\Core;
 use ApiPlatform\Metadata\GetCollection;
 use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProviderInterface;
-use App\ApiResources\Core\File\DownloadRequest;
 use App\Enum\Core\FileStatusEnum;
 use App\Repository\Core\FileRepository;
 use App\Service\File\Exception\FileNotFoundException;
-use App\Service\File\FileManager;
+use App\Service\File\Manager\DocumentManager;
+use App\Service\File\Manager\ImageManager;
+use App\Service\File\Utils\FileUtils;
 use RuntimeException;
 use Symfony\Component\HttpFoundation\HeaderUtils;
 use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -22,8 +23,10 @@ use Symfony\Component\HttpFoundation\Response;
 final class DownloadRequestProvider implements ProviderInterface
 {
     public function __construct(
-        private readonly FileRepository $fileRepository,
-        private readonly FileManager    $fileManager,
+        private readonly FileRepository      $fileRepository,
+        private readonly DocumentManager $documentManager,
+        private readonly ImageManager $imageManager,
+        private readonly FileUtils $fileUtils
     ) {}
 
     /**
@@ -39,15 +42,17 @@ final class DownloadRequestProvider implements ProviderInterface
             throw new RuntimeException('not supported', 500);
         }
 
-        return $this->serveFile($uriVariables['fileId']);
+        $filters = array_key_exists('filters', $context) ? $context['filters'] : [];
+        return $this->serveFile($uriVariables['fileId'], $filters);
     }
 
     /**
      * @param int $fileId
+     * @param array<string, mixed> $filters
      * @return Response
      * @throws FileNotFoundException
      */
-    protected function serveFile(int $fileId): Response {
+    protected function serveFile(int $fileId, array $filters): Response {
         $file = $this->fileRepository->find($fileId);
 
         if (empty($file)) {
@@ -57,7 +62,7 @@ final class DownloadRequestProvider implements ProviderInterface
             throw new RuntimeException("File " . $fileId . " has " . $file->getStatus() . " status; abort.");
         }
 
-        $content = $this->fileManager->read($file);
+        $content = $this->getContent($file, $filters);
 
         // Build the response and attach the file to it
         // @see https://symfony.com/doc/current/components/http_foundation.html#serving-files
@@ -78,4 +83,28 @@ final class DownloadRequestProvider implements ProviderInterface
         return $response;
     }
 
+    /**
+     * @param $file
+     * @param $filters
+     * @return string
+     * @throws FileNotFoundException
+     */
+    private function getContent($file, $filters){
+        if($this->fileUtils->isImage($file)){
+            $height = 0;
+            $width = 0;
+
+            if(array_key_exists('height', $filters)){
+                $height = intval($filters['height']);
+            }
+            if(array_key_exists('width', $filters)){
+                $width = intval($filters['width']);
+            }
+            $content = $this->imageManager->read($file, $height, $width);
+        }else{
+            $content = $this->documentManager->read($file);
+        }
+
+        return $content;
+    }
 }

+ 3 - 3
tests/Unit/Service/Export/LicenceCmfExporterTest.php

@@ -16,7 +16,7 @@ use App\Repository\Organization\OrganizationRepository;
 use App\Service\Export\Encoder\PdfEncoder;
 use App\Service\Export\LicenceCmfExporter;
 use App\Service\Export\Model\LicenceCmf;
-use App\Service\File\FileManager;
+use App\Service\File\Manager\AbstractFileManager;
 use App\Service\File\Storage\LocalStorage;
 use App\Service\ServiceIterator\EncoderIterator;
 use App\Tests\Unit\TestToolsTrait;
@@ -37,7 +37,7 @@ class LicenceCmfExporterTest extends TestCase
     private MockObject | EncoderIterator $encoderIterator;
     private MockObject | EntityManagerInterface $em;
     private MockObject | LocalStorage $localStorage;
-    private MockObject | FileManager $fileManager;
+    private MockObject | AbstractFileManager $fileManager;
     private MockObject | OrganizationRepository $organizationRepo;
     private MockObject | Access $access;
     private MockObject | Organization $organization;
@@ -60,7 +60,7 @@ class LicenceCmfExporterTest extends TestCase
         $this->encoder = $this->getMockBuilder(PdfEncoder::class)->disableOriginalConstructor()->getMock();
         $this->em = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
         $this->localStorage = $this->getMockBuilder(LocalStorage::class)->disableOriginalConstructor()->getMock();
-        $this->fileManager = $this->getMockBuilder(FileManager::class)->disableOriginalConstructor()->getMock();
+        $this->fileManager = $this->getMockBuilder(AbstractFileManager::class)->disableOriginalConstructor()->getMock();
         $this->organizationRepo = $this->getMockBuilder(OrganizationRepository::class)->disableOriginalConstructor()->getMock();
         $this->access = $this->getMockBuilder(Access::class)->getMock();
         $this->organization = $this->getMockBuilder(Organization::class)->getMock();

+ 19 - 19
tests/Unit/Service/File/FileManagerTest.php

@@ -9,7 +9,7 @@ use App\ApiResources\Core\File\DownloadRequest;
 use App\Entity\Core\File;
 use App\Enum\Core\FileHostEnum;
 use App\Service\File\Exception\FileNotFoundException;
-use App\Service\File\FileManager;
+use App\Service\File\Manager\AbstractFileManager;
 use App\Service\File\Storage\ApiLegacyStorage;
 use App\Service\File\Storage\FileStorageInterface;
 use App\Service\File\Storage\LocalStorage;
@@ -29,7 +29,7 @@ class FileManagerTest extends TestCase
     }
 
     public function getFileManagerMockFor(string $methodName) {
-        return $this->getMockBuilder(FileManager::class)
+        return $this->getMockBuilder(AbstractFileManager::class)
             ->setConstructorArgs([$this->localStorage, $this->apiLegacyStorage, $this->iriConverter])
             ->setMethodsExcept([$methodName])
             ->getMock();
@@ -86,29 +86,29 @@ class FileManagerTest extends TestCase
      * @see LocalStorage::guessMimeTypeFromFilename()
      */
     public function testGuessMimeTypeFromFilename(): void {
-        $this->assertEquals('application/pdf', FileManager::guessMimeTypeFromFilename('file.pdf'));
-        $this->assertEquals('text/csv', FileManager::guessMimeTypeFromFilename('file.csv'));
-        $this->assertEquals('text/plain', FileManager::guessMimeTypeFromFilename('file.txt'));
-        $this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', FileManager::guessMimeTypeFromFilename('file.xlsx'));
-        $this->assertEquals('application/xml', FileManager::guessMimeTypeFromFilename('file.xml'));
-
-        $this->assertEquals(null, FileManager::guessMimeTypeFromFilename('file'));
-        $this->assertEquals(null, FileManager::guessMimeTypeFromFilename('file.invalid'));
+        $this->assertEquals('application/pdf', AbstractFileManager::guessMimeTypeFromFilename('file.pdf'));
+        $this->assertEquals('text/csv', AbstractFileManager::guessMimeTypeFromFilename('file.csv'));
+        $this->assertEquals('text/plain', AbstractFileManager::guessMimeTypeFromFilename('file.txt'));
+        $this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', AbstractFileManager::guessMimeTypeFromFilename('file.xlsx'));
+        $this->assertEquals('application/xml', AbstractFileManager::guessMimeTypeFromFilename('file.xml'));
+
+        $this->assertEquals(null, AbstractFileManager::guessMimeTypeFromFilename('file'));
+        $this->assertEquals(null, AbstractFileManager::guessMimeTypeFromFilename('file.invalid'));
     }
 
     /**
      * @see LocalStorage::getMimeTypeFromExt()
      */
     public function testGuessMimeTypeFromExt(): void {
-        $this->assertEquals('application/pdf', FileManager::getMimeTypeFromExt('pdf'));
-        $this->assertEquals('text/csv', FileManager::getMimeTypeFromExt('csv'));
-        $this->assertEquals('text/plain', FileManager::getMimeTypeFromExt('txt'));
-        $this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', FileManager::getMimeTypeFromExt('xlsx'));
-        $this->assertEquals('application/xml', FileManager::getMimeTypeFromExt('xml'));
-
-        $this->assertEquals('text/plain', FileManager::getMimeTypeFromExt('.txt'));
-        $this->assertEquals(null, FileManager::getMimeTypeFromExt(''));
-        $this->assertEquals(null, FileManager::getMimeTypeFromExt('invalid'));
+        $this->assertEquals('application/pdf', AbstractFileManager::getMimeTypeFromExt('pdf'));
+        $this->assertEquals('text/csv', AbstractFileManager::getMimeTypeFromExt('csv'));
+        $this->assertEquals('text/plain', AbstractFileManager::getMimeTypeFromExt('txt'));
+        $this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', AbstractFileManager::getMimeTypeFromExt('xlsx'));
+        $this->assertEquals('application/xml', AbstractFileManager::getMimeTypeFromExt('xml'));
+
+        $this->assertEquals('text/plain', AbstractFileManager::getMimeTypeFromExt('.txt'));
+        $this->assertEquals(null, AbstractFileManager::getMimeTypeFromExt(''));
+        $this->assertEquals(null, AbstractFileManager::getMimeTypeFromExt('invalid'));
     }
 
     /**

+ 3 - 3
tests/Unit/Service/File/Storage/LocalStorageTest.php

@@ -10,7 +10,7 @@ use App\Enum\Core\FileStatusEnum;
 use App\Enum\Core\FileTypeEnum;
 use App\Repository\Access\AccessRepository;
 use App\Service\File\Storage\LocalStorage;
-use App\Service\File\Utils\ImageUtils;
+use App\Service\File\Utils\FileUtils;
 use DateTime;
 use Doctrine\ORM\EntityManagerInterface;
 use Gaufrette\Filesystem;
@@ -37,14 +37,14 @@ class LocalStorageTest extends TestCase
     private EntityManagerInterface $entityManager;
     private AccessRepository $accessRepository;
     private Filesystem $filesystem;
-    private ImageUtils $imageUtils;
+    private FileUtils $imageUtils;
 
     public function setUp(): void
     {
         $this->filesystemMap = $this->getMockBuilder(FilesystemMap::class)->disableOriginalConstructor()->getMock();
         $this->entityManager = $this->getMockBuilder(EntityManagerInterface::class)->disableOriginalConstructor()->getMock();
         $this->accessRepository = $this->getMockBuilder(AccessRepository::class)->disableOriginalConstructor()->getMock();
-        $this->imageUtils = $this->getMockBuilder(ImageUtils::class)->disableOriginalConstructor()->getMock();
+        $this->imageUtils = $this->getMockBuilder(FileUtils::class)->disableOriginalConstructor()->getMock();
 
         $this->filesystem = $this->getMockBuilder(Filesystem::class)->disableOriginalConstructor()->getMock();
         $this->filesystemMap->method('get')->with(TestableLocalStorage::FS_KEY)->willReturn($this->filesystem);

+ 12 - 12
tests/Unit/Service/File/Utils/ImageUtilsTest.php

@@ -3,7 +3,7 @@
 namespace App\tests\Unit\Service\File\Utils;
 
 use App\Entity\Core\File;
-use App\Service\File\Utils\ImageUtils;
+use App\Service\File\Utils\FileUtils;
 use App\Tests\Unit\TestToolsTrait;
 use Liip\ImagineBundle\Imagine\Data\DataManager;
 use Liip\ImagineBundle\Imagine\Filter\FilterManager;
@@ -28,16 +28,16 @@ class ImageUtilsTest extends TestCase
     }
 
     public function getImageUtilsMockFor(string $methodName) {
-        return $this->getMockBuilder(ImageUtils::class)
+        return $this->getMockBuilder(FileUtils::class)
             ->setConstructorArgs([$this->dataManager, $this->filterManager, $this->logger])
             ->setMethodsExcept([$methodName])
             ->getMock();
     }
 
     /**
-     * @see ImageUtils::getCroppingConfig()
      * @return void
      * @throws \ReflectionException
+     *@see FileUtils::getCroppingConfig()
      */
     public function testGetCroppingConfig(): void {
         $imageUtils = $this->getImageUtilsMockFor('getCroppingConfig');
@@ -57,8 +57,8 @@ class ImageUtilsTest extends TestCase
 
 
     /**
-     * @see ImageUtils::formatImage()
      * @return void
+     *@see FileUtils::formatImage()
      */
     public function testFormatImageWithoutConfig()
     {
@@ -73,7 +73,7 @@ class ImageUtilsTest extends TestCase
 
         $this->dataManager->expects($this->once())
             ->method('find')
-            ->with(ImageUtils::FILTER_CROP, $file->getPath())
+            ->with(FileUtils::FILTER_CROP, $file->getPath())
             ->willReturn($binary);
 
         $result = $imageUtils->formatImage($file);
@@ -82,8 +82,8 @@ class ImageUtilsTest extends TestCase
     }
 
     /**
-     * @see ImageUtils::formatImage()
      * @return void
+     *@see FileUtils::formatImage()
      */
     public function testFormatImageWithConfig()
     {
@@ -106,12 +106,12 @@ class ImageUtilsTest extends TestCase
 
         $this->dataManager->expects($this->once())
             ->method('find')
-            ->with(ImageUtils::FILTER_CROP, $file->getPath())
+            ->with(FileUtils::FILTER_CROP, $file->getPath())
             ->willReturn($binary);
 
         $this->filterManager->expects($this->once())
             ->method('applyFilter')
-            ->with($binary, ImageUtils::FILTER_CROP, $config)
+            ->with($binary, FileUtils::FILTER_CROP, $config)
             ->willReturn($binary);
 
         $this->logger->expects($this->never())->method('error');
@@ -123,8 +123,8 @@ class ImageUtilsTest extends TestCase
 
 
     /**
-     * @see ImageUtils::formatImage()
      * @return void
+     *@see FileUtils::formatImage()
      */
     public function testFormatImageWithErrorConfig()
     {
@@ -139,7 +139,7 @@ class ImageUtilsTest extends TestCase
 
         $this->dataManager->expects($this->once())
             ->method('find')
-            ->with(ImageUtils::FILTER_CROP, $file->getPath())
+            ->with(FileUtils::FILTER_CROP, $file->getPath())
             ->willReturn($binary);
 
         $this->logger->expects($this->exactly(2))
@@ -152,8 +152,8 @@ class ImageUtilsTest extends TestCase
     }
 
     /**
-     * @see ImageUtils::isImage()
      * @return void
+     *@see FileUtils::isImage()
      */
     public function testIsImage()
     {
@@ -166,8 +166,8 @@ class ImageUtilsTest extends TestCase
     }
 
     /**
-     * @see ImageUtils::isImage()
      * @return void
+     * @see FileUtils::isImage()
      */
     public function testIsNotImage()
     {

+ 3 - 3
tests/Unit/Service/Twig/AssetsExtensionTest.php

@@ -3,17 +3,17 @@
 namespace App\Tests\Unit\Service\Twig;
 
 use App\Entity\Core\File;
-use App\Service\File\FileManager;
+use App\Service\File\Manager\AbstractFileManager;
 use App\Service\Twig\AssetsExtension;
 use App\Service\Utils\Path;
 use PHPUnit\Framework\TestCase;
 
 class AssetsExtensionTest extends TestCase
 {
-    private FileManager $fileManager;
+    private AbstractFileManager $fileManager;
 
     public function setUp(): void {
-        $this->fileManager = $this->getMockBuilder(FileManager::class)->disableOriginalConstructor()->getMock();
+        $this->fileManager = $this->getMockBuilder(AbstractFileManager::class)->disableOriginalConstructor()->getMock();
     }
 
     public function testGetFunctions(): void {