Pārlūkot izejas kodu

merge feature/v8-4104

Olivier Massot 1 gadu atpakaļ
vecāks
revīzija
74ec526cec
42 mainītis faili ar 907 papildinājumiem un 159 dzēšanām
  1. 9 0
      .gitignore
  2. 2 1
      composer.json
  3. 9 10
      config/bundles.php
  4. 1 1
      config/opentalent/products.yaml
  5. 3 1
      config/packages/knp_gaufrette.yaml
  6. 55 0
      config/packages/liip_imagine.yaml
  7. 2 0
      config/routes/liip_imagine.yaml
  8. 6 0
      config/services.yaml
  9. 6 0
      env/.env.docker
  10. 7 1
      env/.env.prod
  11. 7 1
      env/.env.staging
  12. 7 1
      env/.env.test
  13. 7 1
      env/.env.test1
  14. 7 1
      env/.env.test2
  15. 7 1
      env/.env.test3
  16. 7 1
      env/.env.test4
  17. 7 1
      env/.env.test5
  18. 8 0
      phpstan.dist.neon
  19. 4 12
      src/ApiResources/Core/File/Download.php
  20. 57 0
      src/ApiResources/Core/File/Image.php
  21. 1 1
      src/ApiResources/Core/File/Upload.php
  22. 2 2
      src/Entity/Core/File.php
  23. 11 0
      src/Enum/Core/FileSizeEnum.php
  24. 6 1
      src/Service/Export/BaseExporter.php
  25. 74 0
      src/Service/File/Factory/ImageFactory.php
  26. 34 52
      src/Service/File/FileManager.php
  27. 30 7
      src/Service/File/Storage/ApiLegacyStorage.php
  28. 4 2
      src/Service/File/Storage/FileStorageInterface.php
  29. 73 5
      src/Service/File/Storage/LocalStorage.php
  30. 42 0
      src/Service/ServiceIterator/StorageIterator.php
  31. 5 6
      src/Service/Twig/AssetsExtension.php
  32. 52 0
      src/Service/Utils/FileUtils.php
  33. 35 7
      src/Service/Utils/UrlBuilder.php
  34. 4 4
      src/State/Processor/Core/UploadRequestProcessor.php
  35. 4 5
      src/State/Provider/Core/FileProvider.php
  36. 67 0
      src/State/Provider/Core/ImageProvider.php
  37. 3 3
      templates/export/licence_cmf.html.twig
  38. 3 2
      tests/Unit/Service/File/FileManagerTest.php
  39. 52 26
      tests/Unit/Service/File/Storage/LocalStorageTest.php
  40. 181 0
      tests/Unit/Service/File/Utils/ImageUtilsTest.php
  41. 3 3
      tests/Unit/Service/Twig/AssetsExtensionTest.php
  42. 3 0
      tests/ci_docker_install.sh

+ 9 - 0
.gitignore

@@ -9,6 +9,7 @@
 /config/secrets/prod/prod.decrypt.private.php
 /public/bundles/
 /var/
+/files/
 /vendor/
 ###< symfony/framework-bundle ###
 
@@ -46,3 +47,11 @@ public/phpstorm_debug_validator.phar
 public/phpstorm_debug.php
 
 public/phpstorm_index.php
+
+###> phpstan/phpstan ###
+phpstan.neon
+###< phpstan/phpstan ###
+
+###> liip/imagine-bundle ###
+/public/media/cache/
+###< liip/imagine-bundle ###

+ 2 - 1
composer.json

@@ -29,10 +29,12 @@
     "knplabs/knp-snappy-bundle": "^1.9",
     "lcobucci/jwt": "^4.1",
     "lexik/jwt-authentication-bundle": "^2.8",
+    "liip/imagine-bundle": "^2.12",
     "lorenzo/pinky": "^1.0",
     "myclabs/php-enum": "^1.7",
     "nelmio/cors-bundle": "^2.1",
     "odolbeau/phone-number-bundle": "^3.1",
+    "opentalent/phpdocx": "dev-master",
     "phpdocumentor/reflection-docblock": "^5.2",
     "ramsey/uuid": "^4.2",
     "ramsey/uuid-doctrine": "^2.0",
@@ -67,7 +69,6 @@
     "twig/extra-bundle": "^3.4",
     "twig/inky-extra": "^3.4",
     "vincent/foselastica": "1.3.1",
-    "opentalent/phpdocx": "dev-master",
     "webonyx/graphql-php": "^14.3",
     "xantios/mimey": "*"
   },

+ 9 - 10
config/bundles.php

@@ -1,7 +1,5 @@
 <?php
 
-$devEnvs = ['dev' => true, 'docker' => true, 'test' => false, 'staging' => true];
-
 return [
     Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
     Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
@@ -19,12 +17,13 @@ return [
     Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
     Symfony\Bundle\MercureBundle\MercureBundle::class => ['all' => true],
     Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
-    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => $devEnvs,
-    Symfony\Bundle\MakerBundle\MakerBundle::class => $devEnvs,
-    Symfony\Bundle\DebugBundle\DebugBundle::class => $devEnvs,
-    Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => $devEnvs,
-    Zenstruck\Foundry\ZenstruckFoundryBundle::class => $devEnvs,
-    Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => $devEnvs,
-    Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => $devEnvs,
-    Hautelook\AliceBundle\HautelookAliceBundle::class => $devEnvs,
+    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'docker' => true, 'test' => false, 'staging' => true],
+    Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true, 'docker' => true, 'test' => false, 'staging' => true],
+    Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'docker' => true, 'test' => false, 'staging' => true],
+    Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'docker' => true, 'test' => false, 'staging' => true],
+    Zenstruck\Foundry\ZenstruckFoundryBundle::class => ['dev' => true, 'docker' => true, 'test' => false, 'staging' => true],
+    Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['dev' => true, 'docker' => true, 'test' => false, 'staging' => true],
+    Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['dev' => true, 'docker' => true, 'test' => false, 'staging' => true],
+    Hautelook\AliceBundle\HautelookAliceBundle::class => ['dev' => true, 'docker' => true, 'test' => false, 'staging' => true],
+    Liip\ImagineBundle\LiipImagineBundle::class => ['all' => true],
 ];

+ 1 - 1
config/opentalent/products.yaml

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

+ 3 - 1
config/packages/knp_gaufrette.yaml

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

+ 55 - 0
config/packages/liip_imagine.yaml

@@ -0,0 +1,55 @@
+services:
+    opentalent.liip_imagine.binary.loader.stream.profile_photos:
+        class: Liip\ImagineBundle\Binary\Loader\StreamLoader
+        arguments:
+            - 'gaufrette://storage/'
+        tags:
+            - { name: 'liip_imagine.binary.loader', loader: 'stream.storage' }
+
+# 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:
+        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_resize:
+                    widen: 100
+        crop_md:
+            filters:
+                crop:
+                    size: ~
+                    start: ~
+                relative_resize:
+                    widen: 300
+        crop_lg:
+            filters:
+                crop:
+                    size: ~
+                    start: ~
+                relative_resize:
+                    widen: 800

+ 2 - 0
config/routes/liip_imagine.yaml

@@ -0,0 +1,2 @@
+_liip_imagine:
+    resource: "@LiipImagineBundle/Resources/config/routing.yaml"

+ 6 - 0
config/services.yaml

@@ -21,6 +21,8 @@ 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)%'
+            $baseUrl: '%env(PUBLIC_API_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
@@ -72,6 +74,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
@@ -85,6 +89,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 ##

+ 6 - 0
env/.env.docker

@@ -9,8 +9,14 @@ CORS_ALLOW_ORIGIN=^https?:\/\/(localhost|127\.0\.0\.1|(local.(admin|app|app|fram
 
 ###> api v1 ###
 API_LEG_BASE_URL=http://nginx/
+PUBLIC_API_LEG_BASE_URL=https://local.api.opentalent.fr
 ###< api v1 ###
 
+###> api v2 ###
+API_BASE_URL=http://nginx_new/
+PUBLIC_API_BASE_URL=https://local.ap2i.opentalent.fr
+###< api v2 ###
+
 ###> typo3 client ###
 TYPO3_BASE_URI=http://docker.sub.opentalent.fr
 ###< typo3 client ###

+ 7 - 1
env/.env.prod

@@ -3,7 +3,13 @@ APP_ENV=prod
 
 ###> api v1 ###
 API_LEG_BASE_URL=https://api.opentalent.fr/api
-###< files management ###
+PUBLIC_API_LEG_BASE_URL=https://api.opentalent.fr/api
+###
+
+###> url v2 ###
+API_BASE_URL=https://ap2i.opentalent.fr/api
+PUBLIC_API_BASE_URL=https://ap2i.opentalent.fr/api
+###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=http://ohcluses.opentalent.fr

+ 7 - 1
env/.env.staging

@@ -8,7 +8,13 @@ CORS_ALLOW_ORIGIN=^$
 
 ####> api v1 ###
 API_LEG_BASE_URL=https://none
-####< api v1 ###
+PUBLIC_API_LEG_BASE_URL=https://none
+###< api v1 ###
+
+###> api v2 ###
+API_BASE_URL=https://ap2i.ci.opentalent.fr/api
+PUBLIC_API_BASE_URL=https://ap2i.ci.opentalent.fr
+###< api v2 ###
 
 ###> elasticsearch ###
 ELASTICSEARCH_HOST=es

+ 7 - 1
env/.env.test

@@ -5,7 +5,13 @@ APP_DEBUG=1
 
 ###> api v1 ###
 API_LEG_BASE_URL=https://api.test.opentalent.fr/api
-###< files management ###
+PUBLIC_API_LEG_BASE_URL=https://api.test.opentalent.fr/api
+###< api v1 ###
+
+###> api v2 ###
+API_BASE_URL=https://ap2i.test.opentalent.fr/api
+PUBLIC_API_BASE_URL=https://ap2i.test.opentalent.fr/api
+###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=http://test.opentalent.fr/ohcluses

+ 7 - 1
env/.env.test1

@@ -5,7 +5,13 @@ APP_DEBUG=1
 
 ###> api v1 ###
 API_LEG_BASE_URL=https://api.test1.opentalent.fr/api
-###< files management ###
+PUBLIC_API_LEG_BASE_URL=https://api.test1.opentalent.fr/api
+###< api v1 ###
+
+###> api v2 ###
+API_BASE_URL=https://ap2i.test1.opentalent.fr/api
+PUBLIC_API_BASE_URL=https://ap2i.test1.opentalent.fr/api
+###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=http://test1.opentalent.fr/ohcluses

+ 7 - 1
env/.env.test2

@@ -5,7 +5,13 @@ APP_DEBUG=1
 
 ###> api v1 ###
 API_LEG_BASE_URL=https://api.test2.opentalent.fr/api
-###< files management ###
+PUBLIC_API_LEG_BASE_URL=https://api.test2.opentalent.fr/api
+###< api v1 ###
+
+###> api v2 ###
+API_BASE_URL=https://ap2i.test2.opentalent.fr/api
+PUBLIC_API_BASE_URL=https://ap2i.test2.opentalent.fr/api
+###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=http://test2.opentalent.fr/ohcluses

+ 7 - 1
env/.env.test3

@@ -5,7 +5,13 @@ APP_DEBUG=1
 
 ###> api v1 ###
 API_LEG_BASE_URL=https://api.test3.opentalent.fr/api
-###< files management ###
+PUBLIC_API_LEG_BASE_URL=https://api.test3.opentalent.fr/api
+###< api v1 ###
+
+###> api v2 ###
+API_BASE_URL=https://ap2i.test3.opentalent.fr/api
+PUBLIC_API_BASE_URL=https://ap2i.test3.opentalent.fr/api
+###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=http://test3.opentalent.fr/ohcluses

+ 7 - 1
env/.env.test4

@@ -5,7 +5,13 @@ APP_DEBUG=1
 
 ###> api v1 ###
 API_LEG_BASE_URL=https://api.test4.opentalent.fr/api
-###< files management ###
+PUBLIC_API_LEG_BASE_URL=https://api.test4.opentalent.fr/api
+###< api v1 ###
+
+###> api v2 ###
+API_BASE_URL=https://ap2i.test4.opentalent.fr/api
+PUBLIC_API_BASE_URL=https://ap2i.test4.opentalent.fr/api
+###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=http://test4.opentalent.fr/ohcluses

+ 7 - 1
env/.env.test5

@@ -5,7 +5,13 @@ APP_DEBUG=1
 
 ###> api v1 ###
 API_LEG_BASE_URL=https://api.test5.opentalent.fr/api
-###< files management ###
+PUBLIC_API_LEG_BASE_URL=https://api.test5.opentalent.fr/api
+###< api v1 ###
+
+###> api v2 ###
+API_BASE_URL=https://ap2i.test5.opentalent.fr/api
+PUBLIC_API_BASE_URL=https://ap2i.test5.opentalent.fr/api
+###< api v2 ###
 
 ###> typo3 client ###
 TYPO3_BASE_URI=http://test5.opentalent.fr/ohcluses

+ 8 - 0
phpstan.dist.neon

@@ -0,0 +1,8 @@
+parameters:
+    level: 6
+    paths:
+        - bin/
+        - config/
+        - public/
+        - src/
+        - tests/

+ 4 - 12
src/ApiResources/Core/File/DownloadRequest.php → src/ApiResources/Core/File/Download.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 Download
 {
     #[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;
+    }
+
+}

+ 1 - 1
src/ApiResources/Core/File/UploadRequest.php → src/ApiResources/Core/File/Upload.php

@@ -27,7 +27,7 @@ use Symfony\Component\Validator\Constraints as Assert;
         )
     ]
 )]
-class UploadRequest
+class Upload
 {
     /**
      * @var int | null

+ 2 - 2
src/Entity/Core/File.php

@@ -350,12 +350,12 @@ class File
         return $this;
     }
 
-    public function getConfig(): string
+    public function getConfig(): ?string
     {
         return $this->config;
     }
 
-    public function setConfig(string $config): self
+    public function setConfig(?string $config): self
     {
         $this->config = $config;
         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';
+}

+ 6 - 1
src/Service/Export/BaseExporter.php

@@ -10,6 +10,7 @@ use App\Repository\Access\AccessRepository;
 use App\Repository\Core\FileRepository;
 use App\Service\Export\Model\ExportModelInterface;
 use App\Service\File\FileManager;
+use App\Service\Utils\FileUtils;
 use App\Service\ServiceIterator\EncoderIterator;
 use App\Service\Utils\StringsUtils;
 use Doctrine\ORM\EntityManagerInterface;
@@ -30,6 +31,7 @@ abstract class BaseExporter
     protected EntityManagerInterface $entityManager;
     protected LoggerInterface $logger;
     protected FileManager $fileManager;
+    protected FileUtils $fileUtils;
 
     #[Required]
     public function setAccessRepository(AccessRepository $accessRepository): void
@@ -52,6 +54,9 @@ abstract class BaseExporter
     #[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
     {
@@ -127,7 +132,7 @@ abstract class BaseExporter
             $requester,
             true,
             'NOBODY',
-            FileManager::getMimeTypeFromExt($exportRequest->getFormat()),
+            $this->fileUtils->getMimeTypeFromExt($exportRequest->getFormat()),
             $flushFile
         );
     }

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

@@ -0,0 +1,74 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\File\Factory;
+
+use App\Entity\Core\File;
+use Liip\ImagineBundle\Imagine\Cache\CacheManager;
+use Liip\ImagineBundle\Imagine\Data\DataManager;
+use Liip\ImagineBundle\Imagine\Filter\FilterManager;
+
+/**
+ * Utilise Liip Imagine Bundle afin de créer les images selon une configuration et les stock dans un cache
+ */
+class ImageFactory
+{
+    public function __construct(
+        private readonly DataManager $dataManager,
+        private readonly FilterManager $filterManager,
+        private readonly CacheManager $cacheManager
+){}
+    /**
+     * 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
+     */
+    public function createImageContent(File $file, string $filterName): void{
+        $filters_options = $this->getCropFilterOptions($file->getConfig());
+        $path = $file->getPath();
+        $this->createAndStore($path, $filterName, $filters_options);
+    }
+
+    /**
+     * 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 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|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{
+        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(
+                [
+                    'crop' => array(
+                        'size' => [intval($config['width']), intval($config['height'])],
+                        'start' => [intval($config['x']), intval($config['y'])]
+                    )
+                ],
+                $crop_filters_options
+            );
+        }
+
+        return $crop_filters_options;
+    }
+}

+ 34 - 52
src/Service/File/FileManager.php

@@ -4,21 +4,13 @@ declare(strict_types=1);
 namespace App\Service\File;
 
 use ApiPlatform\Api\IriConverterInterface;
-use ApiPlatform\Api\UrlGeneratorInterface;
+use ApiPlatform\Api\UrlGeneratorInterface as UrlGeneratorInterfaceApiPlatform;
 use ApiPlatform\Metadata\Get;
-use App\ApiResources\Core\File\DownloadRequest;
-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\FileTypeEnum;
 use App\Service\File\Exception\FileNotFoundException;
-use App\Service\File\Storage\ApiLegacyStorage;
+use App\Service\File\Factory\ImageFactory;
 use App\Service\File\Storage\FileStorageInterface;
-use App\Service\File\Storage\LocalStorage;
-use Mimey\MimeTypes;
-use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+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
@@ -27,14 +19,13 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
 class FileManager
 {
     public function __construct(
-        private LocalStorage $localStorage,
-        private ApiLegacyStorage $apiLegacyStorage,
-        protected IriConverterInterface $iriConverter
+        protected readonly IriConverterInterface $iriConverter,
+        protected readonly StorageIterator $storageIterator,
+        protected readonly ImageFactory $imageFactory
     ) {}
 
     /**
      * 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
@@ -42,13 +33,7 @@ class FileManager
      */
     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() . ')');
+        return $this->storageIterator->getStorageFor($file);
     }
 
     /**
@@ -64,6 +49,31 @@ class FileManager
         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);
+    }
     /**
      * Prepare a File record with a PENDING status.
      * This record will hold all the data needed to create the file, except its content.
@@ -148,38 +158,10 @@ class FileManager
     public function getDownloadIri(File $file): string
     {
         return $this->iriConverter->getIriFromResource(
-            DownloadRequest::class,
-            UrlGeneratorInterface::ABS_PATH,
+            File::class,
+            UrlGeneratorInterfaceApiPlatform::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);
-    }
 }

+ 30 - 7
src/Service/File/Storage/ApiLegacyStorage.php

@@ -5,20 +5,22 @@ namespace App\Service\File\Storage;
 
 use App\Entity\Access\Access;
 use App\Entity\Core\File;
+use App\Enum\Core\FileHostEnum;
 use App\Service\ApiLegacy\ApiLegacyRequestService;
 use App\Service\Utils\UrlBuilder;
-use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
-use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
-use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
-use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
-use Symfony\Contracts\HttpClient\HttpClientInterface;
+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)
+    public function __construct(
+        private readonly ApiLegacyRequestService $apiLegacyRequestService,
+        protected readonly DataManager $dataManager,
+        protected readonly UrlBuilder $urlBuilder,
+        protected readonly string $legacyBaseUrl
+    )
     {}
 
     public function exists(File $file): bool {
@@ -33,7 +35,28 @@ 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);
     }
+
+    /**
+     * 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 getImageUrl(File $file, string $size, bool $relativePath): string{
+        $url = sprintf('api/files/%s/download/%s?relativePath=1', $file->getId(), $size);
+        return UrlBuilder::concat($this->legacyBaseUrl, [$this->apiLegacyRequestService->getContent($url)], []);
+    }
+
+    /**
+     * @param File $file
+     * @return bool
+     */
+    public function support(File $file): bool
+    {
+        return $file->getHost() === FileHostEnum::API1()->getValue();
+    }
 }

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

@@ -3,12 +3,14 @@ declare(strict_types=1);
 
 namespace App\Service\File\Storage;
 
-use App\Entity\Access\Access;
 use App\Entity\Core\File;
+use App\Enum\Core\FileSizeEnum;
+use Liip\ImagineBundle\Binary\BinaryInterface;
 
 interface FileStorageInterface
 {
     public function exists(File $file): bool;
-
     public function read(File $file): string;
+    public function getImageUrl(File $file, string $size, bool $relativePath): string;
+    public function support(File $file): bool;
 }

+ 73 - 5
src/Service/File/Storage/LocalStorage.php

@@ -7,17 +7,23 @@ 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\FileSizeEnum;
 use App\Enum\Core\FileStatusEnum;
 use App\Enum\Core\FileTypeEnum;
 use App\Repository\Access\AccessRepository;
-use App\Service\File\FileManager;
+use App\Service\File\Factory\ImageFactory;
+use App\Service\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\Imagine\Cache\CacheManager;
+use Liip\ImagineBundle\Imagine\Data\DataManager;
 use RuntimeException;
 
 /**
@@ -30,12 +36,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 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);
@@ -79,6 +98,46 @@ class LocalStorage implements FileStorageInterface
         return $this->filesystem->read($file->getSlug());
     }
 
+    /**
+     * 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 getImageUrl(File $file, string $size, bool $relativePath): string
+    {
+        $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 FileSizeEnum::SM :
+                $filter = $configExist ? self::CROP_SM : self::SM_FOLDER;
+                break;
+            case FileSizeEnum::MD :
+            default:
+                $filter = $configExist ? self::CROP_MD : self::MD_FOLDER;
+                break;
+            case FileSizeEnum::LG :
+                $filter = $configExist ? self::CROP_LG : self::LG_FOLDER;
+                break;
+        }
+        return $filter;
+    }
+
     /**
      * Prepare a File record with a PENDING status.
      * This record will hold all the data needed to create the file, except its content.
@@ -114,7 +173,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());
@@ -322,4 +381,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();
+    }
 }

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

+ 5 - 6
src/Service/Twig/AssetsExtension.php

@@ -16,7 +16,6 @@ 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
 {
@@ -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->fileManager->read($file));
+        return $this->fileManager->readImage($file, $size, true);
     }
 }

+ 52 - 0
src/Service/Utils/FileUtils.php

@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Service\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
+    {
+        $mimetype = $file->getMimeType() ?: $this->guessMimeTypeFromFilename($file->getName());
+        return boolval(preg_match('#^image#', $mimetype));
+    }
+}

+ 35 - 7
src/Service/Utils/UrlBuilder.php

@@ -3,21 +3,31 @@ declare(strict_types=1);
 
 namespace App\Service\Utils;
 
+use Symfony\Component\Routing\Generator\UrlGenerator;
+
 /**
  * Building url utilities
  */
 class UrlBuilder
 {
+
+    public function __construct(private string $baseUrl){}
+
     /**
      * Concatenate a base url and a path
      *
-     * @param string $url The base url
-     * @param string $path The following path
+     * @param string $base The base url
+     * @param array $tails La suite de l'URL sous forme de tableau
      * @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 = trim($url) . '/' . trim(strval($tail));
+        }
+
+        return $url;
     }
 
     /**
@@ -66,17 +76,35 @@ class UrlBuilder
      * Build an url
      *
      * @param string $url The base url
-     * @param string $path A path to append (can be an empty string)
+     * @param array $tails la suite de l'url sous forme de tableau
      * @param list<string> $parameters A list of parameters (can be an empty array)
      * @param bool $preprendHttps Should the 'https://' be prepended if missing
      * @return string
      */
-    public static function concat(string $url, string $path, array $parameters, bool $preprendHttps = false): string
+    public static function concat(string $url, array $tails, array $parameters, bool $preprendHttps = false): string
     {
-        $url = self::concatParameters(self::concatPath($url, $path), $parameters);
+        $url = self::concatParameters(self::concatPath($url, $tails), $parameters);
         if ($preprendHttps) {
             $url = self::prependHttps($url);
         }
         return $url;
     }
+
+    /**
+     * Retourne l'URL relative sans le scheme et l'host
+     * @param string $path
+     * @return string
+     */
+    public function getRelativeUrl(string $path): string{
+        return UrlGenerator::getRelativePath($this->baseUrl, $path);
+    }
+
+    /**
+     * Retourne l'URL absolue avec le scheme et l'host
+     * @param string $path
+     * @return string
+     */
+    public function getAbsoluteUrl(string $path): string{
+        return self::concat($this->baseUrl, [$path], []);
+    }
 }

+ 4 - 4
src/State/Processor/Core/UploadRequestProcessor.php

@@ -9,7 +9,7 @@ use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProcessorInterface;
 use ApiPlatform\State\ProviderInterface;
 use App\ApiResources\Core\File\DownloadRequest;
-use App\ApiResources\Core\File\UploadRequest;
+use App\ApiResources\Core\File\Upload;
 use App\Entity\Access\Access;
 use App\Enum\Core\FileStatusEnum;
 use App\Enum\Core\FileTypeEnum;
@@ -36,10 +36,10 @@ final class UploadRequestProcessor implements ProcessorInterface
      * @param Operation $operation
      * @param array<mixed> $uriVariables
      * @param array<mixed> $context
-     * @return UploadRequest
+     * @return Upload
      * @throws FileNotFoundException
      */
-    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): UploadRequest
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Upload
     {
         if ($operation instanceof Delete){
             throw new RuntimeException('not supported', Response::HTTP_METHOD_NOT_ALLOWED);
@@ -48,7 +48,7 @@ final class UploadRequestProcessor implements ProcessorInterface
         /** @var Access $author */
         $author = $this->security->getUser();
 
-        /** @var UploadRequest $uploadRequest */
+        /** @var Upload $uploadRequest */
         $uploadRequest = $data;
 
         if (empty($uploadRequest->getFilename())) {

+ 4 - 5
src/State/Provider/Core/DownloadRequestProvider.php → src/State/Provider/Core/FileProvider.php

@@ -6,7 +6,6 @@ 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 Symfony\Component\HttpFoundation\Response;
 use App\Repository\Core\FileRepository;
@@ -19,11 +18,11 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
 /**
  * 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 FileManager    $fileManager,
+        private readonly FileRepository      $fileRepository,
+        private readonly FileManager $fileManager
     ) {}
 
     /**
@@ -44,6 +43,7 @@ final class DownloadRequestProvider implements ProviderInterface
 
     /**
      * @param int $fileId
+     * @param array<string, mixed> $filters
      * @return Response
      * @throws FileNotFoundException
      */
@@ -77,5 +77,4 @@ final class DownloadRequestProvider implements ProviderInterface
 
         return $response;
     }
-
 }

+ 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\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 %}

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

@@ -7,12 +7,13 @@ use ApiPlatform\Api\UrlGeneratorInterface;
 use ApiPlatform\Metadata\Get;
 use App\ApiResources\Core\File\DownloadRequest;
 use App\Entity\Access\Access;
+use App\ApiResources\Core\File\Download;
 use App\Entity\Core\File;
 use App\Entity\Organization\Organization;
 use App\Enum\Core\FileHostEnum;
 use App\Enum\Core\FileTypeEnum;
 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;
@@ -186,7 +187,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(

+ 52 - 26
tests/Unit/Service/File/Storage/LocalStorageTest.php

@@ -2,7 +2,6 @@
 
 namespace App\Tests\Unit\Service\File\Storage;
 
-use ApiPlatform\Core\Api\IriConverterInterface;
 use App\Entity\Access\Access;
 use App\Entity\Core\File;
 use App\Entity\Organization\Organization;
@@ -11,6 +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\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 IriConverterInterface $iriConverter;
+    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->iriConverter = $this->getMockBuilder(IriConverterInterface::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);
@@ -55,7 +55,7 @@ class LocalStorageTest extends TestCase
      */
     public function testExists(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['exists'])
             ->getMock();
 
@@ -72,7 +72,7 @@ class LocalStorageTest extends TestCase
      */
     public function testExistsInexistant(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['exists'])
             ->getMock();
 
@@ -91,7 +91,7 @@ class LocalStorageTest extends TestCase
     public function testListByOwner(): void
     {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['listByOwner'])
             ->getMock();
 
@@ -110,15 +110,41 @@ class LocalStorageTest extends TestCase
     /**
      * @see LocalStorage::read()
      */
-    public function testRead(): void
+    public function testReadPdf(): void
     {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['read'])
             ->getMock();
 
         $file = $this->getMockBuilder(File::class)->getMock();
         $file->method('getSlug')->willReturn('foo');
+        $file->method('getMimeType')->willReturn('application/pdf');
+
+        $this->filesystem->method('read')->with('foo')->willReturn('12345679');
+
+        $this->assertEquals(
+            '12345679',
+            $fileStorage->read($file)
+        );
+    }
+
+    /**
+     * @see LocalStorage::read()
+     */
+    public function testReadImage(): void
+    {
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getSlug')->willReturn('foo');
+        $file->method('getMimeType')->willReturn('image/jpeg');
+
+        $imageUtils = $this->imageUtils;
+        $imageUtils->method('formatImage')->with($file)->willReturn('12345679');
+
+        $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $imageUtils])
+            ->setMethodsExcept(['read'])
+            ->getMock();
 
         $this->filesystem->method('read')->with('foo')->willReturn('12345679');
 
@@ -134,7 +160,7 @@ class LocalStorageTest extends TestCase
     public function testPrepareFile(): void
     {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['prepareFile'])
             ->getMock();
 
@@ -175,7 +201,7 @@ class LocalStorageTest extends TestCase
     public function testPrepareFileDefaultValues(): void
     {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['prepareFile'])
             ->getMock();
 
@@ -204,7 +230,7 @@ class LocalStorageTest extends TestCase
     public function testPrepareFileNoFlush(): void
     {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['prepareFile'])
             ->getMock();
 
@@ -233,7 +259,7 @@ class LocalStorageTest extends TestCase
      */
     public function testWriteNewFile(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['write'])
             ->getMock();
 
@@ -287,7 +313,7 @@ class LocalStorageTest extends TestCase
      */
     public function testWriteExistingFile(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['write'])
             ->getMock();
 
@@ -340,7 +366,7 @@ class LocalStorageTest extends TestCase
     public function testWriteExistingButMissingFile(): void
     {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['write'])
             ->getMock();
 
@@ -373,7 +399,7 @@ class LocalStorageTest extends TestCase
      */
     public function testWriteWithAccessOwner(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['write'])
             ->getMock();
 
@@ -416,7 +442,7 @@ class LocalStorageTest extends TestCase
     public function testWriteWithNoName(): void
     {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['write'])
             ->getMock();
 
@@ -436,7 +462,7 @@ class LocalStorageTest extends TestCase
      */
     public function testMakeFile(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['makeFile'])
             ->getMock();
 
@@ -472,7 +498,7 @@ class LocalStorageTest extends TestCase
      */
     public function testSoftdelete(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['softDelete'])
             ->getMock();
 
@@ -496,7 +522,7 @@ class LocalStorageTest extends TestCase
      */
     public function testHardDelete(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['hardDelete'])
             ->getMock();
 
@@ -513,7 +539,7 @@ class LocalStorageTest extends TestCase
      */
     public function testHardDeleteFailed(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['hardDelete'])
             ->getMock();
 
@@ -533,7 +559,7 @@ class LocalStorageTest extends TestCase
      */
     public function testGetPrefixAccess(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['getPrefix'])
             ->getMock();
 
@@ -554,7 +580,7 @@ class LocalStorageTest extends TestCase
      */
     public function testGetPrefixOrganization(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['getPrefix'])
             ->getMock();
 
@@ -571,7 +597,7 @@ class LocalStorageTest extends TestCase
      */
     public function testGetPrefixPerson(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['getPrefix'])
             ->getMock();
 
@@ -588,7 +614,7 @@ class LocalStorageTest extends TestCase
      */
     public function testGetPrefixTemp(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['getPrefix'])
             ->getMock();
 
@@ -605,7 +631,7 @@ class LocalStorageTest extends TestCase
      */
     public function testGetPrefixWithType(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['getPrefix'])
             ->getMock();
 
@@ -622,7 +648,7 @@ class LocalStorageTest extends TestCase
      */
     public function testGetOrganizationAndPersonFromOwner(): void {
         $fileStorage = $this->getMockBuilder(TestableLocalStorage::class)
-            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->iriConverter])
+            ->setConstructorArgs([$this->filesystemMap, $this->entityManager, $this->accessRepository, $this->imageUtils])
             ->setMethodsExcept(['getOrganizationAndPersonFromOwner'])
             ->getMock();
 

+ 181 - 0
tests/Unit/Service/File/Utils/ImageUtilsTest.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace App\tests\Unit\Service\File\Utils;
+
+use App\Entity\Core\File;
+use App\Service\File\Utils\FileUtils;
+use App\Tests\Unit\TestToolsTrait;
+use Liip\ImagineBundle\Imagine\Data\DataManager;
+use Liip\ImagineBundle\Imagine\Filter\FilterManager;
+use Liip\ImagineBundle\Model\Binary;
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Psr\Log\LoggerInterface;
+
+class ImageUtilsTest extends TestCase
+{
+    use TestToolsTrait;
+
+    private MockObject | DataManager $dataManager;
+    private MockObject | FilterManager $filterManager;
+    private MockObject | LoggerInterface $logger;
+
+    public function setUp(): void
+    {
+        $this->dataManager = $this->getMockBuilder(DataManager::class)->disableOriginalConstructor()->getMock();
+        $this->filterManager = $this->getMockBuilder(FilterManager::class)->disableOriginalConstructor()->getMock();
+        $this->logger = $this->getMockBuilder(LoggerInterface::class)->disableOriginalConstructor()->getMock();
+    }
+
+    public function getImageUtilsMockFor(string $methodName) {
+        return $this->getMockBuilder(FileUtils::class)
+            ->setConstructorArgs([$this->dataManager, $this->filterManager, $this->logger])
+            ->setMethodsExcept([$methodName])
+            ->getMock();
+    }
+
+    /**
+     * @return void
+     * @throws \ReflectionException
+     *@see FileUtils::getCroppingConfig()
+     */
+    public function testGetCroppingConfig(): void {
+        $imageUtils = $this->getImageUtilsMockFor('getCroppingConfig');
+
+        $config = '{"width": 100, "height": 100, "x": 10, "y": 10}';
+        $result =[
+            'filters' => [
+                'crop' => [
+                    'size' => [100, 100],
+                    'start' => [10, 10]
+                ]
+            ]
+        ];
+
+        $this->assertEquals($result, $this->invokeMethod($imageUtils, 'getCroppingConfig', [$config]));
+    }
+
+
+    /**
+     * @return void
+     *@see FileUtils::formatImage()
+     */
+    public function testFormatImageWithoutConfig()
+    {
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getPath')->willReturn('example.jpg');
+        $file->method('getConfig')->willReturn(null);
+
+        $imageUtils = $this->getImageUtilsMockFor('formatImage');
+
+        $binary = $this->createMock(Binary::class);
+        $binary->method('getContent')->willReturn('mocked_binary_data');
+
+        $this->dataManager->expects($this->once())
+            ->method('find')
+            ->with(FileUtils::FILTER_CROP, $file->getPath())
+            ->willReturn($binary);
+
+        $result = $imageUtils->formatImage($file);
+
+        $this->assertEquals('mocked_binary_data', $result);
+    }
+
+    /**
+     * @return void
+     *@see FileUtils::formatImage()
+     */
+    public function testFormatImageWithConfig()
+    {
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getPath')->willReturn('example.jpg');
+        $file->method('getConfig')->willReturn('{"width": 100, "height": 100, "x": 10, "y": 10}');
+        $config  = [
+            'filters' => [
+                'crop' => [
+                    'size' => [100, 100],
+                    'start' => [10, 10]
+                ]
+            ]
+        ];
+
+        $imageUtils = $this->getImageUtilsMockFor('formatImage');
+
+        $binary = $this->createMock(Binary::class);
+        $binary->method('getContent')->willReturn('mocked_binary_data');
+
+        $this->dataManager->expects($this->once())
+            ->method('find')
+            ->with(FileUtils::FILTER_CROP, $file->getPath())
+            ->willReturn($binary);
+
+        $this->filterManager->expects($this->once())
+            ->method('applyFilter')
+            ->with($binary, FileUtils::FILTER_CROP, $config)
+            ->willReturn($binary);
+
+        $this->logger->expects($this->never())->method('error');
+
+        $result = $imageUtils->formatImage($file);
+
+        $this->assertEquals('mocked_binary_data', $result);
+    }
+
+
+    /**
+     * @return void
+     *@see FileUtils::formatImage()
+     */
+    public function testFormatImageWithErrorConfig()
+    {
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getPath')->willReturn('example.jpg');
+        $file->method('getConfig')->willReturn('{"hei": 100, "x": 10, "y": 10}');
+
+        $imageUtils = $this->getImageUtilsMockFor('formatImage');
+
+        $binary = $this->createMock(Binary::class);
+        $binary->method('getContent')->willReturn('mocked_binary_data');
+
+        $this->dataManager->expects($this->once())
+            ->method('find')
+            ->with(FileUtils::FILTER_CROP, $file->getPath())
+            ->willReturn($binary);
+
+        $this->logger->expects($this->exactly(2))
+            ->method('error')
+        ;
+
+        $result = $imageUtils->formatImage($file);
+
+        $this->assertEquals('mocked_binary_data', $result);
+    }
+
+    /**
+     * @return void
+     *@see FileUtils::isImage()
+     */
+    public function testIsImage()
+    {
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getMimeType')->willReturn('image/jpeg');
+
+        $imageUtils = $this->getImageUtilsMockFor('isImage');
+
+        $this->assertTrue($imageUtils->isImage($file));
+    }
+
+    /**
+     * @return void
+     * @see FileUtils::isImage()
+     */
+    public function testIsNotImage()
+    {
+        $file = $this->getMockBuilder(File::class)->getMock();
+        $file->method('getMimeType')->willReturn('application/pdf');
+
+        $imageUtils = $this->getImageUtilsMockFor('isImage');
+
+        $this->assertFalse($imageUtils->isImage($file));
+    }
+}

+ 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 {

+ 3 - 0
tests/ci_docker_install.sh

@@ -37,6 +37,9 @@ rm -r /var/lib/apt/lists/*
 #docker-php-ext-install xsl
 #pecl install apcu-5.1.21
 
+pecl install imagick
+docker-php-ext-enable imagick
+
 # Install XDebug (required for coverage)
 pecl install xdebug
 docker-php-ext-enable xdebug