浏览代码

Merge branch 'V8-6652_suppression_fichiers_v2' into feature/V8-3988-suppression-orgas

Olivier Massot 10 月之前
父节点
当前提交
be0a6fad82

+ 2 - 2
config/packages/knp_gaufrette.yaml

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

+ 1 - 0
config/services.yaml

@@ -24,6 +24,7 @@ services:
             $legacyBaseUrl: '%env(PUBLIC_API_LEG_BASE_URL)%'
             $baseUrl: '%env(PUBLIC_API_BASE_URL)%'
             $opentalentMailReport: 'mail.report@opentalent.fr'
+            $fileStorageDir: '%kernel.project_dir%/var/files/storage'
 
     # makes classes in src/ available to be used as services
     # this creates a service per class whose id is the fully-qualified class name

+ 20 - 0
src/Repository/Core/FileRepository.php

@@ -14,4 +14,24 @@ class FileRepository extends ServiceEntityRepository
     {
         parent::__construct($registry, File::class);
     }
+
+    public function deleteByOrganization(int $organizationId): void
+    {
+        $this->createQueryBuilder('f')
+            ->delete()
+            ->where('f.organization = :organizationId')
+            ->setParameter('organizationId', $organizationId)
+            ->getQuery()
+            ->execute();
+    }
+
+    public function deleteByPerson(int $personId): void
+    {
+        $this->createQueryBuilder('f')
+            ->delete()
+            ->where('f.person = :personId')
+            ->setParameter('personId', $personId)
+            ->getQuery()
+            ->execute();
+    }
 }

+ 37 - 1
src/Service/File/FileManager.php

@@ -13,11 +13,13 @@ use App\Entity\Organization\Organization;
 use App\Entity\Person\Person;
 use App\Enum\Core\FileTypeEnum;
 use App\Enum\Core\FileVisibilityEnum;
+use App\Repository\Core\FileRepository;
 use App\Service\File\Exception\FileNotFoundException;
 use App\Service\File\Factory\ImageFactory;
 use App\Service\File\Storage\FileStorageInterface;
 use App\Service\File\Storage\LocalStorage;
 use App\Service\ServiceIterator\StorageIterator;
+use Doctrine\ORM\EntityManagerInterface;
 
 /**
  * Le gestionnaire de fichiers permet d'effectuer de nombreuses opérations sur les fichiers stockés dans les différents
@@ -29,7 +31,9 @@ class FileManager
         protected readonly IriConverterInterface $iriConverter,
         protected readonly StorageIterator $storageIterator,
         protected readonly ImageFactory $imageFactory,
-        protected readonly LocalStorage $localStorage
+        protected readonly LocalStorage $localStorage,
+        protected readonly EntityManagerInterface $entityManager,
+        protected readonly FileRepository $fileRepository,
     ) {
     }
 
@@ -137,4 +141,36 @@ class FileManager
             ['fileId' => $file->getId()]
         );
     }
+
+    /**
+     * Permanently delete the organization's files from each storage, and remove any reference
+     * in the DB
+     *
+     * @param int $organizationId
+     * @return void
+     */
+    public function deleteOrganizationFiles(int $organizationId): void
+    {
+        foreach ($this->storageIterator->getStorages() as $storageService) {
+            $storageService->deleteOrganizationFiles($organizationId);
+        }
+
+        $this->fileRepository->deleteByOrganization($organizationId);
+    }
+
+    /**
+     * Permanently delete the person's files from each storage, and remove any reference
+     * * in the DB
+ *
+     * @param int $personId
+     * @return void
+     */
+    public function deletePersonFiles(int $personId): void
+    {
+        foreach ($this->storageIterator->getStorages() as $storageService) {
+            $storageService->deletePersonFiles($personId);
+        }
+
+        $this->fileRepository->deleteByPerson($personId);
+    }
 }

+ 24 - 0
src/Service/File/Storage/ApiLegacyStorage.php

@@ -52,4 +52,28 @@ class ApiLegacyStorage implements FileStorageInterface
     {
         return $file->getHost() === FileHostEnum::API1;
     }
+
+    /**
+     * Permanently delete the entire file storage of the given Organization
+     *
+     * @param int $organizationId
+     * @return void
+     */
+    public function deleteOrganizationFiles(int $organizationId): void
+    {
+        $url = sprintf('/_internal/request/organization-files/delete/%s', $organizationId);
+        $this->apiLegacyRequestService->get($url);
+    }
+
+    /**
+     * Permanently delete the entire file storage of the given Person
+     *
+     * @param int $personId
+     * @return void
+     */
+    public function deletePersonFiles(int $personId): void
+    {
+        $url = sprintf('/_internal/request/person-files/delete/%s', $personId);
+        $this->apiLegacyRequestService->get($url);
+    }
 }

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

@@ -15,4 +15,8 @@ interface FileStorageInterface
     public function getImageUrl(File $file, string $size, bool $relativePath): string;
 
     public function support(File $file): bool;
+
+    public function deleteOrganizationFiles(int $organizationId): void;
+
+    public function deletePersonFiles(int $personId): void;
 }

+ 49 - 7
src/Service/File/Storage/LocalStorage.php

@@ -47,14 +47,15 @@ class LocalStorage implements FileStorageInterface
     protected FilesystemInterface $filesystem;
 
     public function __construct(
-        protected readonly FilesystemMap $filesystemMap,
+        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
+        protected readonly AccessRepository       $accessRepository,
+        protected readonly DataManager            $dataManager,
+        protected readonly CacheManager           $cacheManager,
+        protected readonly ImageFactory           $imageFactory,
+        protected readonly FileUtils              $fileUtils,
+        protected readonly UrlBuilder             $urlBuilder,
+        protected readonly string                 $fileStorageDir
     ) {
         $this->filesystem = $filesystemMap->get(static::FS_KEY);
     }
@@ -294,6 +295,47 @@ class LocalStorage implements FileStorageInterface
         }
     }
 
+    /**
+     * Permanently delete the entire file storage of the given Organization
+     *
+     * @param int $organizationId
+     * @return void
+     */
+    public function deleteOrganizationFiles(int $organizationId): void
+    {
+        $this->rrmDir('organization/' . $organizationId);
+        $this->rrmDir('temp/organization/' . $organizationId);
+    }
+
+    /**
+     * Permanently delete the entire file storage of the given Person
+     *
+     * @param int $personId
+     * @return void
+     */
+    public function deletePersonFiles(int $personId): void
+    {
+        $this->rrmDir('person/' . $personId);
+        $this->rrmDir('temp/person/' . $personId);
+    }
+
+    /**
+     * Supprime récursivement un répertoire
+     *
+     * (Au moment du développement, Gaufrette ne permet pas la suppression de répertoire, on laissera
+     * le soin à un cron de supprimer les répertoires vides du storage)
+     *
+     * @param string $dirKey
+     * @return void
+     */
+    protected function rrmDir(string $dirKey): void {
+        if (!$this->filesystem->isDirectory($dirKey)) {
+            throw new \RuntimeException('Directory `'.$dirKey.'` does not exist');
+        }
+        $dir = Path::join($this->fileStorageDir, $dirKey);
+        Path::rmtree($dir);
+    }
+
     /**
      * If an organization owns the file, the prefix will be '(_temp_/)organization/{id}(/{type})'.
      * If a person owns it, the prefix will be '(_temp_/)person/{id}(/{type})'

+ 36 - 42
src/Service/Organization/OrganizationFactory.php

@@ -26,6 +26,7 @@ use App\Repository\Core\CountryRepository;
 use App\Repository\Organization\OrganizationRepository;
 use App\Repository\Person\PersonRepository;
 use App\Service\Dolibarr\DolibarrApiService;
+use App\Service\File\FileManager;
 use App\Service\Organization\Utils as OrganizationUtils;
 use App\Service\Typo3\BindFileService;
 use App\Service\Typo3\SubdomainService;
@@ -57,10 +58,9 @@ class OrganizationFactory
         private readonly Typo3Service $typo3Service,
         private readonly DolibarrApiService $dolibarrApiService,
         private readonly EntityManagerInterface $entityManager,
-        private readonly PersonRepository $personRepository,
-        private readonly BindFileService $bindFileService,
-    ) {
-    }
+        private readonly PersonRepository       $personRepository,
+        private readonly BindFileService        $bindFileService, private readonly FileManager $fileManager,
+    ) {}
 
     #[Required]
     /** @see https://symfony.com/doc/current/logging/channels_handlers.html#how-to-autowire-logger-channels */
@@ -596,6 +596,9 @@ class OrganizationFactory
         SecurityUtils::preventIfNotLocalhost();
 
         $organization = $this->organizationRepository->find($organizationDeletionRequest->getOrganizationId());
+        if (!$organization) {
+            throw new \RuntimeException("No organization was found for id : " . $organizationDeletionRequest->getOrganizationId());
+        }
 
         $this->logger->info(
             "Start the deletion of organization '".$organization->getName()."' [".$organization->getId().']'
@@ -606,8 +609,7 @@ class OrganizationFactory
         $withError = false;
 
         try {
-            // On doit gérer à part la suppression des Access afin de supprimer au passage les Person devenues 'orphelines'
-            $this->deleteOrganizationAccesses($organization);
+            $orphanPersons = $this->getOrphansToBePersons($organization);
 
             // On est obligé de supprimer manuellement les paramètres, car c'est l'entité Parameters qui est
             // propriétaire de la relation Organization ↔ Parameters.
@@ -616,6 +618,13 @@ class OrganizationFactory
             // Toutes les autres entités liées seront supprimées en cascade
             $this->entityManager->remove($organization);
 
+            // Supprime les personnes qui n'avaient pas d'autre Access attaché
+            $deletedPersonIds = [];
+            foreach ($orphanPersons as $person) {
+                $deletedPersonIds[] = $person->getId();
+                $this->entityManager->remove($person);
+            }
+
             $this->entityManager->flush();
             $this->entityManager->commit();
         } catch (\Exception $e) {
@@ -625,7 +634,7 @@ class OrganizationFactory
         }
 
         try {
-            $this->deleteTypo3Website($organization);
+            $this->deleteTypo3Website($organizationDeletionRequest->getOrganizationId());
         } catch (\Exception $e) {
             $this->logger->critical('An error happened while deleting the Typo3 website, please proceed manually.');
             $this->logger->debug($e);
@@ -641,27 +650,21 @@ class OrganizationFactory
         }
 
         try {
-            $this->deleteLocalDirectories($organization);
-        } catch (\Exception $e) {
-            $this->logger->critical('An error happened while deleting the local directories, please proceed manually.');
-            $this->logger->debug($e);
-            $withError = true;
-        }
-
-        try {
-            $this->deleteDirectoriesV1($organization);
+            $this->fileManager->deleteOrganizationFiles($organizationDeletionRequest->getOrganizationId());
         } catch (\Exception $e) {
-            $this->logger->critical('An error happened while deleting the V1 directories, please proceed manually.');
+            $this->logger->critical("An error happened while deleting the organization's files, please proceed manually.");
             $this->logger->debug($e);
             $withError = true;
         }
 
-        try {
-            $this->deleteDirectories59($organization);
-        } catch (\Exception $e) {
-            $this->logger->critical('An error happened while deleting the 5.9 directories, please proceed manually.');
-            $this->logger->debug($e);
-            $withError = true;
+        foreach ($deletedPersonIds as $personId) {
+            try {
+                $this->fileManager->deletePersonFiles($personId);
+            } catch (\Exception $e) {
+                $this->logger->critical("An error happened while deleting the person's files, please proceed manually (id=" . $person->getId() . ").");
+                $this->logger->debug($e);
+                $withError = true;
+            }
         }
 
         if ($withError) {
@@ -677,16 +680,21 @@ class OrganizationFactory
     /**
      * Supprime tous les Access d'une organisation, ainsi que la Person
      * rattachée (si celle-ci n'est pas liée à d'autres Access).
+     *
+     * @param Organization $organization
+     * @return array<Person>
      */
-    protected function deleteOrganizationAccesses(Organization $organization): void
+    protected function getOrphansToBePersons(Organization $organization): array
     {
+        $orphans = [];
+
         foreach ($organization->getAccesses() as $access) {
             $person = $access->getPerson();
             if ($person->getAccesses()->count() === 1) {
-                $this->entityManager->remove($person);
+                $orphans[] = $person;
             }
-            $this->entityManager->remove($access);
         }
+        return $orphans;
     }
 
     // TODO: à revoir, c'est du many to many
@@ -711,7 +719,8 @@ class OrganizationFactory
     //        }
     //    }
 
-    protected function deleteTypo3Website(Organization $organization): void
+
+    protected function deleteTypo3Website(int $organizationId): void
     {
         // TODO: implement
         //        $this->typo3Service->deleteSite($organization->getId());
@@ -721,19 +730,4 @@ class OrganizationFactory
     {
         $this->dolibarrApiService->switchSocietyToProspect($organization->getId());
     }
-
-    protected function deleteLocalDirectories(Organization $organization): void
-    {
-        // TODO: implement
-    }
-
-    protected function deleteDirectoriesV1(Organization $organization): void
-    {
-        // TODO: implement
-    }
-
-    protected function deleteDirectories59(Organization $organization): void
-    {
-        // TODO: implement
-    }
 }

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

@@ -22,6 +22,11 @@ class StorageIterator
     ) {
     }
 
+    public function getStorages(): iterable
+    {
+        return $this->storageServices;
+    }
+
     /**
      * Itère sur les services de storage disponibles et
      * retourne le premier qui supporte ce type de requête.

+ 1 - 1
src/Service/Utils/Path.php

@@ -96,7 +96,7 @@ class Path
      *
      * @return bool returns true if the directory was successfully removed, false otherwise
      */
-    protected static function rmtree(string $path): bool
+    public static function rmtree(string $path): bool
     {
         if (!file_exists($path)) {
             return true;

+ 5 - 5
tests/Unit/Service/Organization/OrganizationFactoryTest.php

@@ -146,9 +146,9 @@ class TestableOrganizationFactory extends OrganizationFactory
         parent::switchDolibarrSocietyToProspect($organization);
     }
 
-    public function deleteLocalDirectories(Organization $organization): void
+    public function deleteOrganizationFiles(Organization $organization): void
     {
-        parent::deleteLocalDirectories($organization);
+        parent::deleteOrganizationFiles($organization);
     }
 
     public function deleteDirectoriesV1(Organization $organization): void
@@ -1346,7 +1346,7 @@ class OrganizationFactoryTest extends TestCase
 
         $organizationFactory->expects(self::once())->method('deleteTypo3Website')->with($organization);
         $organizationFactory->expects(self::once())->method('switchDolibarrSocietyToProspect')->with($organization);
-        $organizationFactory->expects(self::once())->method('deleteLocalDirectories')->with($organization);
+        $organizationFactory->expects(self::once())->method('deleteOrganizationFiles')->with($organization);
         $organizationFactory->expects(self::once())->method('deleteDirectoriesV1')->with($organization);
         $organizationFactory->expects(self::once())->method('deleteDirectories59')->with($organization);
 
@@ -1392,7 +1392,7 @@ class OrganizationFactoryTest extends TestCase
 
         $organizationFactory->expects(self::never())->method('deleteTypo3Website');
         $organizationFactory->expects(self::never())->method('switchDolibarrSocietyToProspect');
-        $organizationFactory->expects(self::never())->method('deleteLocalDirectories');
+        $organizationFactory->expects(self::never())->method('deleteOrganizationFiles');
         $organizationFactory->expects(self::never())->method('deleteDirectoriesV1');
         $organizationFactory->expects(self::never())->method('deleteDirectories59');
 
@@ -1445,7 +1445,7 @@ class OrganizationFactoryTest extends TestCase
 
         $organizationFactory->expects(self::once())->method('deleteTypo3Website')->willThrowException(new \Exception('some error'));
         $organizationFactory->expects(self::once())->method('switchDolibarrSocietyToProspect')->willThrowException(new \Exception('some error'));
-        $organizationFactory->expects(self::once())->method('deleteLocalDirectories')->willThrowException(new \Exception('some error'));
+        $organizationFactory->expects(self::once())->method('deleteOrganizationFiles')->willThrowException(new \Exception('some error'));
         $organizationFactory->expects(self::once())->method('deleteDirectoriesV1')->willThrowException(new \Exception('some error'));
         $organizationFactory->expects(self::once())->method('deleteDirectories59')->willThrowException(new \Exception('some error'));