Bladeren bron

ajout du cache de liip et changement de l'architecture du file manager

Vincent 2 jaren geleden
bovenliggende
commit
9b84d3ee27

+ 5 - 0
.env.ci

@@ -40,3 +40,8 @@ LOG_FILE_NAME=ci
 
 ###> api v1 ###
 API_LEG_BASE_URL=https://api.ci.opentalent.fr/api
+###
+
+###> url v1 ###
+PUBLIC_API_LEG_BASE_URL=https://api.ci.opentalent.fr
+###

+ 5 - 1
.env.docker

@@ -14,6 +14,10 @@ CORS_ALLOW_ORIGIN=^https?:\/\/(localhost|127\.0\.0\.1|(local.(admin|app|app\-v3|
 API_LEG_BASE_URL=http://nginx/
 ###< api v1 ###
 
+###> url v1 ###
+PUBLIC_API_LEG_BASE_URL=https://local.api.opentalent.fr
+###
+
 ###> BlackFire configuration ###
 BLACKFIRE_CLIENT_ID=988fcba8-552d-48df-a9c2-035c76535b69
 BLACKFIRE_CLIENT_TOKEN=8cfbeb263d044da9678dc2612531504da3790c308da7448e35724a5da91c136f
@@ -57,4 +61,4 @@ LOG_FILE_NAME=docker
 
 ### Internal requests (@see doc/internal_requests.md)
 INTERNAL_FILES_DOWNLOAD_URI=https://nginx/_internal/secure/files
-###
+###

+ 4 - 0
.env.prod

@@ -12,6 +12,10 @@ CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)$
 API_LEG_BASE_URL=https://api.opentalent.fr/api
 ###< files management ###
 
+###> url v1 ###
+PUBLIC_API_LEG_BASE_URL=https://api.opentalent.fr/api
+###
+
 ###> BlackFire configuration ###
 BLACKFIRE_CLIENT_ID=988fcba8-552d-48df-a9c2-035c76535b69
 BLACKFIRE_CLIENT_TOKEN=8cfbeb263d044da9678dc2612531504da3790c308da7448e35724a5da91c136f

+ 4 - 0
.env.staging

@@ -13,6 +13,10 @@ CORS_ALLOW_ORIGIN=^$
 API_LEG_BASE_URL=https://none
 ####< api v1 ###
 
+###> url v1 ###
+PUBLIC_API_LEG_BASE_URL=https://none
+###
+
 ###> elasticsearch ###
 ELASTICSEARCH_HOST=es
 ELASTICSEARCH_PORT=9200

+ 4 - 0
.env.test

@@ -14,6 +14,10 @@ CORS_ALLOW_ORIGIN=^https?://(localhost|127\.0\.0\.1)(:[0-9]+)$
 API_LEG_BASE_URL=https://api.test.opentalent.fr/api
 ###< files management ###
 
+###> url v1 ###
+PUBLIC_API_LEG_BASE_URL=https://api.test.opentalent.fr/api
+###
+
 ###> typo3 client ###
 TYPO3_BASE_URI=http://test.opentalent.fr/ohcluses
 ###< typo3 client ###

+ 1 - 0
.gitignore

@@ -9,6 +9,7 @@
 /config/secrets/prod/prod.decrypt.private.php
 /public/bundles/
 /var/
+/files/
 /vendor/
 ###< symfony/framework-bundle ###
 

+ 1 - 1
config/opentalent/products.yaml

@@ -9,12 +9,12 @@ parameters:
           - ContactPoint
           - PersonalizedList
           - File
+          - Image
           - City
           - Country
           - Tagg
           - Enum
           - LicenceCmfOrganizationER
-          - DownloadRequest
           - SubdomainAvailability
         roles:
           - ROLE_IMPORT

+ 2 - 2
config/packages/knp_gaufrette.yaml

@@ -4,10 +4,10 @@ knp_gaufrette:
   adapters:
     storage:
       local:
-        directory: '%kernel.project_dir%/var/files/storage'
+        directory: '%kernel.project_dir%/files/storage'
         create: true
   filesystems:
     storage:
       adapter: storage
 
-  stream_wrapper: ~
+  stream_wrapper: ~

+ 25 - 14
config/packages/liip_imagine.yaml

@@ -8,37 +8,48 @@ services:
 
 # Documentation on how to configure the bundle can be found at: https://symfony.com/doc/current/bundles/LiipImagineBundle/basic-usage.html
 liip_imagine:
+    resolvers:
+        my_resolver:
+            web_path: ~
+
+    cache : my_resolver
+
     # valid drivers options include "gd" or "gmagick" or "imagick"
     driver: "imagick"
 
     data_loader: stream.storage
 
     filter_sets:
-        no_filter: ~
-        crop_filter:
+        sm:
+            filters:
+                relative_resize:
+                    widen: 100
+        md:
+            filters:
+                relative_resize:
+                    widen: 300
+        lg:
+            filters:
+                relative_resize:
+                    widen: 800
+        crop_sm:
             filters:
                 crop:
                     size: ~
                     start: ~
-        relative_width_crop_filter:
+                relative_resize:
+                    widen: 100
+        crop_md:
             filters:
                 crop:
                     size: ~
                     start: ~
                 relative_resize:
-                    widen: ~
-        relative_width_filter:
-            filters:
-                relative_resize:
-                    widen: ~
-        relative_height_crop_filter:
+                    widen: 300
+        crop_lg:
             filters:
                 crop:
                     size: ~
                     start: ~
                 relative_resize:
-                    heighten: ~
-        relative_height_filter:
-            filters:
-                relative_resize:
-                    heighten: ~
+                    widen: 800

+ 1 - 0
config/services.yaml

@@ -20,6 +20,7 @@ services:
             $persistProcessor: '@api_platform.doctrine.orm.state.persist_processor'
             $removeProcessor: '@api_platform.doctrine.orm.state.remove_processor'
             $opentalentNoReplyEmailAddress: 'noreply@opentalent.fr'
+            $legacyBaseUrl: '%env(PUBLIC_API_LEG_BASE_URL)%'
 
     # makes classes in src/ available to be used as services
     # this creates a service per class whose id is the fully-qualified class name

+ 4 - 12
src/ApiResources/Core/File/DownloadRequest.php → src/ApiResources/Core/File/File.php

@@ -6,27 +6,19 @@ namespace App\ApiResources\Core\File;
 use ApiPlatform\Metadata\ApiProperty;
 use ApiPlatform\Metadata\ApiResource;
 use ApiPlatform\Metadata\Get;
-use App\State\Provider\Core\DownloadRequestProvider;
+use App\State\Provider\Core\FileProvider;
 
-/**
- * A request for a file from the LocalStorage
- */
 #[ApiResource(
     operations: [
         new Get(
-            uriTemplate: '/download/{fileId}',
+            uriTemplate: 'file/download/{fileId}',
             requirements: ['fileId' => '\\d+'],
             security: 'is_granted("ROLE_FILE")',
-            provider: DownloadRequestProvider::class
-        ),
-        new Get(
-            uriTemplate: '/internal/download/{fileId}',
-            requirements: ['fileId' => '\\d+'],
-            provider: DownloadRequestProvider::class
+            provider: FileProvider::class
         )
     ]
 )]
-class DownloadRequest
+class File
 {
     #[ApiProperty(identifier: true)]
     private int $fileId;

+ 57 - 0
src/ApiResources/Core/File/Image.php

@@ -0,0 +1,57 @@
+<?php
+declare (strict_types=1);
+
+namespace App\ApiResources\Core\File;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use App\Enum\Core\FileSizeEnum;
+use App\State\Provider\Core\ImageProvider;
+use Symfony\Component\Routing\Requirement\EnumRequirement;
+use Symfony\Component\Validator\Constraints as Assert;
+
+/**
+ * A request for a file from the LocalStorage
+ */
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/image/download/{fileId}/{size}',
+            requirements: ['fileId' => '\\d+', 'size' => new EnumRequirement(FileSizeEnum::class)],
+            security: 'is_granted("ROLE_FILE")',
+            provider: ImageProvider::class
+        )
+    ]
+)]
+class Image
+{
+    #[ApiProperty(identifier: true)]
+    private int $fileId;
+
+    #[Assert\Type(type: FileSizeEnum::class)]
+    private string $size;
+
+    public function getFileId() : int
+    {
+        return $this->fileId;
+    }
+
+    public function setFileId(int $fileId) : self
+    {
+        $this->fileId = $fileId;
+        return $this;
+    }
+
+    public function getSize(): string
+    {
+        return $this->size;
+    }
+
+    public function setSize(string $size): self
+    {
+        $this->size = $size;
+        return $this;
+    }
+
+}

+ 11 - 0
src/Enum/Core/FileSizeEnum.php

@@ -0,0 +1,11 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Enum\Core;
+
+enum FileSizeEnum: string
+{
+    case SM = 'sm';
+    case MD = 'md';
+    case LG = 'lg';
+}

+ 5 - 4
src/Service/Export/BaseExporter.php

@@ -9,6 +9,7 @@ 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\Manager\DocumentManager;
 use App\Service\File\Utils\FileUtils;
 use App\Service\ServiceIterator\EncoderIterator;
@@ -30,7 +31,7 @@ abstract class BaseExporter
     protected EncoderIterator $encoderIterator;
     protected EntityManagerInterface $entityManager;
     protected LoggerInterface $logger;
-    protected DocumentManager $documentManager;
+    protected FileManager $fileManager;
     protected FileUtils $fileUtils;
 
     #[Required]
@@ -49,8 +50,8 @@ abstract class BaseExporter
     public function setEntityManager(EntityManagerInterface $entityManager): void
     { $this->entityManager = $entityManager; }
     #[Required]
-    public function setDocumentManager(DocumentManager $documentManager): void
-    { $this->documentManager = $documentManager; }
+    public function setFileManager(FileManager $fileManager): void
+    { $this->fileManager = $fileManager; }
     #[Required]
     public function setLogger(LoggerInterface $logger): void
     { $this->logger = $logger; }
@@ -101,7 +102,7 @@ abstract class BaseExporter
             $file = $this->prepareFile($exportRequest, false);
         }
 
-        return $this->documentManager->write($file, $content, $requester);
+        return $this->fileManager->write($file, $content, $requester);
     }
 
     /**

+ 32 - 113
src/Service/File/Factory/ImageFactory.php

@@ -4,116 +4,55 @@ declare(strict_types=1);
 
 namespace App\Service\File\Factory;
 
-use Liip\ImagineBundle\Binary\BinaryInterface;
+use App\Entity\Core\File;
+use Liip\ImagineBundle\Imagine\Cache\CacheManager;
+use Liip\ImagineBundle\Imagine\Data\DataManager;
 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
+        private readonly DataManager $dataManager,
+        private readonly FilterManager $filterManager,
+        private readonly CacheManager $cacheManager
 ){}
     /**
-     * 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
+     * Permet de créer les images retaillées
+     * @param File $file : File contenant l'image de base
+     * @param string $filterName : nom du filtre liip à appliquer
+     * @return void
      */
-    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;
+    public function createImageContent(File $file, string $filterName): void{
+        $filters_options = $this->getCropFilterOptions($file->getConfig());
+        $path = $file->getPath();
+        $this->createAndStore($path, $filterName, $filters_options);
     }
 
     /**
-     * Construit les options des filtres à appliquer
-     * @param $params
+     * Créer l'image retaillée et l'à stock l'image dans le cache
+     * @param string $path : chemin de l'image
+     * @param string $filter : nom du filtre liip à appliquer
+     * @param array<string, array<string, array<string, array<int, int>>>> $filtersOptions : options du filtre
+     * @return void
      */
-
-    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));
-        }
+    private function createAndStore(string $path, string $filter, array $filtersOptions){
+        $binary = $this->dataManager->find($filter, $path);
+        $binary = $this->filterManager->applyFilter($binary, $filter, ['filters' => $filtersOptions]);
+        $this->cacheManager->store($binary, $path, $filter);
     }
 
     /**
      * 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
+     * @param string|null $config : Configuration du File
+     * @return array<string, array<string, array<string, array<int, int>>>> | array : tableau de configuration
      * @see ImageUtilsTest::testGetCroppingConfig()
      */
-    protected function getCropFilterOptions(string $config): array{
+    protected function getCropFilterOptions(?string $config): array{
+        if(!$config)
+            return [];
+
         $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(
@@ -122,31 +61,11 @@ class ImageFactory
                         'size' => [intval($config['width']), intval($config['height'])],
                         'start' => [intval($config['x']), intval($config['y'])]
                     )
-                ]
+                ],
+                $crop_filters_options
             );
         }
-        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)
-        )];
+        return $crop_filters_options;
     }
 }

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

@@ -0,0 +1,93 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Service\File;
+
+use ApiPlatform\Api\IriConverterInterface;
+use ApiPlatform\Api\UrlGeneratorInterface as UrlGeneratorInterfaceApiPlatform;
+use ApiPlatform\Metadata\Get;
+use App\Entity\Core\File;
+use App\Service\File\Exception\FileNotFoundException;
+use App\Service\File\Factory\ImageFactory;
+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...)
+ */
+class FileManager
+{
+    public function __construct(
+        protected readonly IriConverterInterface $iriConverter,
+        protected readonly StorageIterator $storageIterator,
+        protected readonly ImageFactory $imageFactory
+    ) {}
+
+    /**
+     * 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);
+    }
+
+    /**
+     * 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);
+    }
+
+    /**
+     * Lit le fichier Image et retourne une URL
+     * @param File $file
+     * @param string $size
+     * @param bool $relativePath
+     * @return string
+     * @throws FileNotFoundException
+     */
+    public function getImageUrl(File $file, string $size, bool $relativePath = false): string
+    {
+        $storage = $this->getStorageFor($file);
+        return $storage->getImageUrl($file, $size, $relativePath);
+    }
+
+    /**
+     * 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(
+            File::class,
+            UrlGeneratorInterfaceApiPlatform::ABS_PATH,
+            new Get(),
+            ['fileId' => $file->getId()]
+        );
+    }
+}

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

@@ -1,53 +0,0 @@
-<?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()]
-        );
-    }
-}

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

@@ -1,35 +0,0 @@
-<?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);
-    }
-}

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

@@ -1,36 +0,0 @@
-<?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);
-    }
-}

+ 13 - 39
src/Service/File/Storage/ApiLegacyStorage.php

@@ -6,7 +6,7 @@ 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 App\Service\Utils\UrlBuilder;
 use Liip\ImagineBundle\Imagine\Data\DataManager;
 
 /**
@@ -14,9 +14,12 @@ use Liip\ImagineBundle\Imagine\Data\DataManager;
  */
 class ApiLegacyStorage implements FileStorageInterface
 {
-    const TMP_FOLDER = 'gaufrette://storage/temp';
-
-    public function __construct(private ApiLegacyRequestService $apiLegacyRequestService, protected DataManager $dataManager,)
+    public function __construct(
+        private readonly ApiLegacyRequestService $apiLegacyRequestService,
+        protected readonly DataManager $dataManager,
+        protected readonly UrlBuilder $urlBuilder,
+        protected readonly string $legacyBaseUrl
+    )
     {}
 
     /**
@@ -32,44 +35,15 @@ class ApiLegacyStorage implements FileStorageInterface
     }
 
     /**
-     * Get the image file and returns its content as a binary
-     *
+     * Retoune l'URL de l'image, à la bonne taille, contenu dans la File
      * @param File $file
+     * @param string $size
+     * @param bool $relativePath
      * @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);
+    public function getImageUrl(File $file, string $size, bool $relativePath): string{
+        $url = sprintf('api/files/%s/download/%s?relativePath=1', $file->getId(), $size);
+        return sprintf('%s%s', $this->legacyBaseUrl, $this->apiLegacyRequestService->getContent($url));
     }
 
     /**

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

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

+ 53 - 12
src/Service/File/Storage/LocalStorage.php

@@ -11,15 +11,17 @@ use App\Enum\Core\FileHostEnum;
 use App\Enum\Core\FileStatusEnum;
 use App\Enum\Core\FileTypeEnum;
 use App\Repository\Access\AccessRepository;
+use App\Service\File\Factory\ImageFactory;
 use App\Service\File\Utils\FileUtils;
 use App\Service\Utils\Path;
+use App\Service\Utils\UrlBuilder;
 use App\Service\Utils\Uuid;
 use DateTime;
 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\Cache\CacheManager;
 use Liip\ImagineBundle\Imagine\Data\DataManager;
 use RuntimeException;
 
@@ -33,14 +35,25 @@ class LocalStorage implements FileStorageInterface
      */
     protected const FS_KEY = 'storage';
 
+    //Cache Image Folder
+    protected const SM_FOLDER = 'sm';
+    protected const MD_FOLDER = 'md';
+    protected const LG_FOLDER = 'lg';
+    protected const CROP_SM = 'crop_sm';
+    protected const CROP_MD = 'crop_md';
+    protected const CROP_LG = 'crop_lg';
+
     protected FilesystemInterface $filesystem;
 
     public function __construct(
-        protected FilesystemMap $filesystemMap,
-        protected EntityManagerInterface $entityManager,
-        protected AccessRepository $accessRepository,
-        protected DataManager $dataManager,
-        protected FileUtils $fileUtils
+        protected readonly FilesystemMap $filesystemMap,
+        protected readonly EntityManagerInterface $entityManager,
+        protected readonly AccessRepository $accessRepository,
+        protected readonly DataManager $dataManager,
+        protected readonly CacheManager $cacheManager,
+        protected readonly ImageFactory $imageFactory,
+        protected readonly FileUtils $fileUtils,
+        protected readonly UrlBuilder $urlBuilder
     )
     {
         $this->filesystem = $filesystemMap->get(static::FS_KEY);
@@ -85,15 +98,43 @@ class LocalStorage implements FileStorageInterface
     }
 
     /**
-     * Get the image file and returns its content as a binary
-     *
+     * Retoune l'URL de l'image, à la bonne taille, contenu dans la File
      * @param File $file
-     * @return BinaryInterface
+     * @param string $size
+     * @param bool $relativePath
+     * @return string
      */
-    public function getBinaryImage(File $file): BinaryInterface
+    public function getImageUrl(File $file, string $size, bool $relativePath): string
     {
-        //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());
+        $filterName = $this->getFilterFromSizeAndConfig($size, !empty($file->getConfig()));
+        $path = $file->getPath();
+        if (!$this->cacheManager->isStored($path, $filterName)) {
+            $this->imageFactory->createImageContent($file, $filterName);
+        }
+        $url = $this->cacheManager->resolve($path, $filterName);
+        return $relativePath ? $this->urlBuilder->getRelativeUrl($url) : $url;
+    }
+
+    /**
+     * Retourne le filtre Liip correspondant à la taille désirée
+     * @param bool $configExist
+     * @param string $size
+     * @return string
+     */
+    private function getFilterFromSizeAndConfig(string $size, bool $configExist): string{
+        switch($size){
+            case 'sm':
+                $filter = $configExist ? self::CROP_SM : self::SM_FOLDER;
+                break;
+            case 'md':
+            default:
+                $filter = $configExist ? self::CROP_MD : self::MD_FOLDER;
+                break;
+            case 'lg':
+                $filter = $configExist ? self::CROP_LG : self::LG_FOLDER;
+                break;
+        }
+        return $filter;
     }
 
     /**

+ 7 - 8
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\Manager\ImageManager;
+use App\Service\File\FileManager;
 use App\Service\Utils\Path;
 use Twig\Extension\AbstractExtension;
 use Twig\TwigFunction;
@@ -16,12 +16,11 @@ use Twig\TwigFunction;
  * This is particularly useful for exports, since wkhtmltoX can't resolve partial paths for assets, or download
  * files from the Opentalent API (the non-public ones at least)
  *
- * // TODO: à voir si c'est bien à sa place parmi les services
  */
 class AssetsExtension extends AbstractExtension
 {
     public function __construct(
-        readonly private ImageManager $imageManager
+        readonly private FileManager $fileManager
     )
     {}
 
@@ -29,7 +28,7 @@ class AssetsExtension extends AbstractExtension
     {
         return [
             new TwigFunction('absPath', [$this, 'absPath']),
-            new TwigFunction('toBase64Src', [$this, 'toBase64Src']),
+            new TwigFunction('fileImagePath', [$this, 'fileImagePath']),
         ];
     }
 
@@ -49,18 +48,18 @@ class AssetsExtension extends AbstractExtension
     }
 
     /**
-     * Return the src of an image as a base64 content
+     * Retourne l'URL d'accès à une image contenu dans une File
      *
      * Usage :
      *
-     *      <img src="{{ toBase64Src((licence.logo) }}"/>
+     *      <img src="{{ fileImagePath((licence.logo, 'sm') }}"/>
      *
      * @param File $file
      * @return string
      * @throws FileNotFoundException
      */
-    public function toBase64Src(File $file): string
+    public function fileImagePath(File $file, $size): string
     {
-        return 'data:' . $file->getMimeType() . ';base64,' . base64_encode($this->imageManager->read($file));
+        return $this->fileManager->readImage($file, $size, true);
     }
 }

+ 32 - 2
src/Service/Utils/UrlBuilder.php

@@ -3,11 +3,17 @@ declare(strict_types=1);
 
 namespace App\Service\Utils;
 
+use Symfony\Component\Routing\Generator\UrlGenerator;
+use Symfony\Component\Routing\RequestContext;
+
 /**
  * Building url utilities
  */
 class UrlBuilder
 {
+
+    public function __construct(private RequestContext $requestContext){}
+
     /**
      * Concatenate a base url and a path
      *
@@ -15,9 +21,14 @@ class UrlBuilder
      * @param string $path The following path
      * @return string
      */
-    public static function concatPath(string $url, string $path): string
+    public static function concatPath(string $base, array $tails): string
     {
-        return rtrim($url, '/') . '/' . ltrim($path, '/');
+        $url = $base;
+        foreach ($tails as $tail){
+            $url = preg_replace('/^\/|\/$/i', '', $url) . '/' . preg_replace('/^\/?|\/?$/i', '', strval($tail));
+        }
+
+        return $url;
     }
 
     /**
@@ -79,4 +90,23 @@ class UrlBuilder
         }
         return $url;
     }
+
+    /**
+     * Retourne l'URL relative sans le scheme et l'host
+     * @param string $path
+     * @return string
+     */
+    public function getRelativeUrl(string $path): string{
+        $baseUrl = sprintf('%s://%s/', $this->requestContext->getScheme(), $this->requestContext->getHost() );
+        return UrlGenerator::getRelativePath($baseUrl, $path);
+    }
+
+    /**
+     * Retourne l'URL absolue avec le scheme et l'host
+     * @param string $path
+     * @return string
+     */
+    public function getAbsoluteUrl(string $path): string{
+        return sprintf('%s://%s/%s', $this->requestContext->getScheme(), $this->requestContext->getHost(), $path );
+    }
 }

+ 6 - 36
src/State/Provider/Core/DownloadRequestProvider.php → src/State/Provider/Core/FileProvider.php

@@ -9,9 +9,7 @@ use ApiPlatform\State\ProviderInterface;
 use App\Enum\Core\FileStatusEnum;
 use App\Repository\Core\FileRepository;
 use App\Service\File\Exception\FileNotFoundException;
-use App\Service\File\Manager\DocumentManager;
-use App\Service\File\Manager\ImageManager;
-use App\Service\File\Utils\FileUtils;
+use App\Service\File\FileManager;
 use RuntimeException;
 use Symfony\Component\HttpFoundation\HeaderUtils;
 use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -20,13 +18,11 @@ use Symfony\Component\HttpFoundation\Response;
 /**
  * Custom provider pour le téléchargement des fichiers du LocalStorage
  */
-final class DownloadRequestProvider implements ProviderInterface
+final class FileProvider implements ProviderInterface
 {
     public function __construct(
         private readonly FileRepository      $fileRepository,
-        private readonly DocumentManager $documentManager,
-        private readonly ImageManager $imageManager,
-        private readonly FileUtils $fileUtils
+        private readonly FileManager $fileManager
     ) {}
 
     /**
@@ -42,8 +38,7 @@ final class DownloadRequestProvider implements ProviderInterface
             throw new RuntimeException('not supported', 500);
         }
 
-        $filters = array_key_exists('filters', $context) ? $context['filters'] : [];
-        return $this->serveFile($uriVariables['fileId'], $filters);
+        return $this->serveFile($uriVariables['fileId']);
     }
 
     /**
@@ -52,7 +47,7 @@ final class DownloadRequestProvider implements ProviderInterface
      * @return Response
      * @throws FileNotFoundException
      */
-    protected function serveFile(int $fileId, array $filters): Response {
+    protected function serveFile(int $fileId): Response {
         $file = $this->fileRepository->find($fileId);
 
         if (empty($file)) {
@@ -62,7 +57,7 @@ final class DownloadRequestProvider implements ProviderInterface
             throw new RuntimeException("File " . $fileId . " has " . $file->getStatus() . " status; abort.");
         }
 
-        $content = $this->getContent($file, $filters);
+        $content = $this->fileManager->read($file);
 
         // Build the response and attach the file to it
         // @see https://symfony.com/doc/current/components/http_foundation.html#serving-files
@@ -82,29 +77,4 @@ 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;
-    }
 }

+ 67 - 0
src/State/Provider/Core/ImageProvider.php

@@ -0,0 +1,67 @@
+<?php
+declare(strict_types=1);
+
+namespace App\State\Provider\Core;
+
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+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\Utils\FileUtils;
+use RuntimeException;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Custom provider pour récupérer l'URL d'une image
+ */
+final class ImageProvider implements ProviderInterface
+{
+    public function __construct(
+        private readonly FileRepository $fileRepository,
+        private readonly FileManager $fileManager,
+        private readonly FileUtils $fileUtils
+    ) {}
+
+    /**
+     * @param Operation $operation
+     * @param array<mixed> $uriVariables
+     * @param array<mixed> $context
+     * @return Response|RedirectResponse
+     * @throws FileNotFoundException
+     */
+    public function provide(Operation $operation, array $uriVariables = [], array $context = []): Response | RedirectResponse
+    {
+        if($operation instanceof GetCollection) {
+            throw new RuntimeException('not supported', 500);
+        }
+        return $this->getImage($uriVariables['fileId'], $uriVariables['size']);
+    }
+
+    /**
+     * @param int $fileId
+     * @param string $size
+     * @return Response
+     * @throws FileNotFoundException
+     */
+    protected function getImage(int $fileId, string $size): Response {
+        $file = $this->fileRepository->find($fileId);
+
+        if (empty($file)) {
+            throw new RuntimeException("Image " . $fileId . " does not exist; abort.");
+        }
+        if ($file->getStatus() !== FileStatusEnum::READY()->getValue()) {
+            throw new RuntimeException("Image " . $fileId . " has " . $file->getStatus() . " status; abort.");
+        }
+        if(!$this->fileUtils->isImage($file)){
+            throw new RuntimeException("File " . $fileId . " is not an image.");
+        }
+
+        $content = $this->fileManager->getImageUrl($file, $size);
+
+        return new Response($content);
+    }
+}

+ 3 - 3
templates/export/licence_cmf.html.twig

@@ -241,7 +241,7 @@
                                              width="85"
                                              height="82"/>
                                     {% else %}
-                                        <img src="{{ toBase64Src(licence.logo) }}"
+                                        <img src="{{ fileImagePath(licence.logo, 'sm') }}"
                                              width="85"
                                              height="82"/>
                                     {% endif %}
@@ -266,7 +266,7 @@
                                                 height="82"/>
                                     {% else %}
                                         <img class="avatar"
-                                             src="{{ toBase64Src(licence.personAvatar) }}"/>
+                                             src="{{ fileImagePath(licence.personAvatar, 'sm') }}"/>
                                     {% endif %}
                                 </div>
                             </td>
@@ -302,7 +302,7 @@
                         <td width="70" align="right" valign="middle" id="qrCode">
                             {% if(licence.qrCode) %}
                                 <img style="margin-right: 10px;"
-                                     src="{{ toBase64Src(licence.qrCode) }}"
+                                     src="{{ fileImagePath(licence.qrCode, 'sm') }}"
                                      alt=""
                                      width="65" height="65"/>
                             {% endif %}

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

@@ -5,7 +5,7 @@ namespace App\Tests\Unit\Service\File;
 use ApiPlatform\Api\IriConverterInterface;
 use ApiPlatform\Api\UrlGeneratorInterface;
 use ApiPlatform\Metadata\Get;
-use App\ApiResources\Core\File\DownloadRequest;
+use App\ApiResources\Core\File\File;
 use App\Entity\Core\File;
 use App\Enum\Core\FileHostEnum;
 use App\Service\File\Exception\FileNotFoundException;
@@ -124,7 +124,7 @@ class FileManagerTest extends TestCase
         $this->iriConverter
             ->expects(self::once())
             ->method('getIriFromResource')
-            ->with(DownloadRequest::class, UrlGeneratorInterface::ABS_PATH, new Get(),['fileId' => 1])
+            ->with(File::class, UrlGeneratorInterface::ABS_PATH, new Get(),['fileId' => 1])
             ->willReturn('/api/download/1');
 
         $this->assertEquals(