Browse Source

implements FileStorage

Olivier Massot 3 years ago
parent
commit
ea60c65786

+ 4 - 4
config/packages/knp_gaufrette.yaml

@@ -2,10 +2,10 @@
 # Documentation : https://knplabs.github.io/Gaufrette/basic-usage.html
 knp_gaufrette:
   adapters:
-    temp:
+    storage:
       local:
-        directory: '%kernel.project_dir%/var/files/temp'
+        directory: '%kernel.project_dir%/var/files/storage'
         create: true
   filesystems:
-    temp:
-      adapter: temp
+    storage:
+      adapter: storage

+ 75 - 51
src/Entity/Core/File.php

@@ -9,7 +9,6 @@ use App\Entity\Booking\Event;
 use App\Entity\Booking\EventReport;
 use App\Entity\Booking\Work;
 use App\Entity\Message\TemplateSystem;
-use App\Entity\Network\Network;
 use App\Entity\Organization\Activity;
 use App\Entity\Organization\OnlineRegistrationSettings;
 use App\Entity\Organization\Organization;
@@ -59,17 +58,17 @@ class File
 
     /**
      * Slug du fichier (i.e. le chemin d'accès relatif)
-     * @var string
+     * @var string|null
      */
     #[ORM\Column(length: 255)]
-    private string $slug;
+    private ?string $slug = null;
 
     /**
      * Chemin d'accès du fichier
-     * @var string
+     * @var string|null
      */
     #[ORM\Column(length: 255)]
-    private string $path;
+    private ?string $path = null;
 
     /**
      * Nom du fichier
@@ -259,10 +258,13 @@ class File
 
     /**
      * @param Person $person
+     * @return File
      */
-    public function setPerson(Person $person): void
+    public function setPerson(Person $person): self
     {
         $this->person = $person;
+
+        return $this;
     }
 
     /**
@@ -275,29 +277,32 @@ class File
 
     /**
      * @param Organization $organization
+     * @return File
      */
-    public function setOrganization(Organization $organization): void
+    public function setOrganization(Organization $organization): self
     {
         $this->organization = $organization;
+
+        return $this;
     }
 
-    public function getSlug(): string
+    public function getSlug(): ?string
     {
         return $this->slug;
     }
 
-    public function setSlug(string $slug): self
+    public function setSlug(?string $slug): self
     {
         $this->slug = $slug;
         return $this;
     }
 
-    public function getPath(): string
+    public function getPath(): ?string
     {
         return $this->path;
     }
 
-    public function setPath(string $path): self
+    public function setPath(?string $path): self
     {
         $this->path = $path;
         return $this;
@@ -354,11 +359,9 @@ class File
 
     public function removePersonImage(Person $person): self
     {
-        if ($this->personImages->removeElement($person)) {
-            // set the owning side to null (unless already changed)
-            if ($person->getImage() === $this) {
-                $person->setImage(null);
-            }
+        // set the owning side to null (unless already changed)
+        if ($this->personImages->removeElement($person) && $person->getImage() === $this) {
+            $person->setImage(null);
         }
 
         return $this;
@@ -374,10 +377,13 @@ class File
 
     /**
      * @param string $visibility
+     * @return File
      */
-    public function setVisibility(string $visibility): void
+    public function setVisibility(string $visibility): self
     {
         $this->visibility = $visibility;
+
+        return $this;
     }
 
     /**
@@ -390,10 +396,13 @@ class File
 
     /**
      * @param string $folder
+     * @return File
      */
-    public function setFolder(string $folder): void
+    public function setFolder(string $folder): self
     {
         $this->folder = $folder;
+
+        return $this;
     }
 
     /**
@@ -406,10 +415,13 @@ class File
 
     /**
      * @param string $type
+     * @return File
      */
-    public function setType(string $type): void
+    public function setType(string $type): self
     {
         $this->type = $type;
+
+        return $this;
     }
 
     /**
@@ -422,10 +434,13 @@ class File
 
     /**
      * @param int|null $size
+     * @return File
      */
-    public function setSize(?int $size): void
+    public function setSize(?int $size): self
     {
         $this->size = $size;
+
+        return $this;
     }
 
     /**
@@ -438,10 +453,13 @@ class File
 
     /**
      * @param bool $isTemporaryFile
+     * @return File
      */
-    public function setIsTemporaryFile(bool $isTemporaryFile): void
+    public function setIsTemporaryFile(bool $isTemporaryFile): self
     {
         $this->isTemporaryFile = $isTemporaryFile;
+
+        return $this;
     }
 
     /**
@@ -454,10 +472,13 @@ class File
 
     /**
      * @param DateTime $createDate
+     * @return File
      */
-    public function setCreateDate(DateTime $createDate): void
+    public function setCreateDate(DateTime $createDate): self
     {
         $this->createDate = $createDate;
+
+        return $this;
     }
 
     /**
@@ -470,10 +491,13 @@ class File
 
     /**
      * @param int|null $createdBy
+     * @return File
      */
-    public function setCreatedBy(?int $createdBy): void
+    public function setCreatedBy(?int $createdBy): self
     {
         $this->createdBy = $createdBy;
+
+        return $this;
     }
 
     /**
@@ -486,10 +510,13 @@ class File
 
     /**
      * @param DateTime $updateDate
+     * @return File
      */
-    public function setUpdateDate(DateTime $updateDate): void
+    public function setUpdateDate(DateTime $updateDate): self
     {
         $this->updateDate = $updateDate;
+
+        return $this;
     }
 
     /**
@@ -502,10 +529,13 @@ class File
 
     /**
      * @param int|null $updatedBy
+     * @return File
      */
-    public function setUpdatedBy(?int $updatedBy): void
+    public function setUpdatedBy(?int $updatedBy): self
     {
         $this->updatedBy = $updatedBy;
+
+        return $this;
     }
 
     /**
@@ -518,10 +548,13 @@ class File
 
     /**
      * @param string|null $status
+     * @return File
      */
-    public function setStatus(?string $status): void
+    public function setStatus(?string $status): self
     {
         $this->status = $status;
+
+        return $this;
     }
 
     public function getOrganizationLogos(): Collection
@@ -541,11 +574,9 @@ class File
 
     public function removeOrganizationLogo(Organization $organization): self
     {
-        if ($this->organizationLogos->removeElement($organization)) {
-            // set the owning side to null (unless already changed)
-            if ($organization->getLogo() === $this) {
-                $organization->setLogo(null);
-            }
+        // set the owning side to null (unless already changed)
+        if ($this->organizationLogos->removeElement($organization) && $organization->getLogo() === $this) {
+            $organization->setLogo(null);
         }
 
         return $this;
@@ -568,11 +599,9 @@ class File
 
     public function removeOrganizationImage(Organization $organization): self
     {
-        if ($this->organizationImages->removeElement($organization)) {
-            // set the owning side to null (unless already changed)
-            if ($organization->getImage() === $this) {
-                $organization->setImage(null);
-            }
+        // set the owning side to null (unless already changed)
+        if ($this->organizationImages->removeElement($organization) && $organization->getImage() === $this) {
+            $organization->setImage(null);
         }
 
         return $this;
@@ -586,6 +615,7 @@ class File
     public function setQrCode(Parameters $qrCode): self
     {
         $this->qrCode = $qrCode;
+
         return $this;
     }
 
@@ -633,11 +663,9 @@ class File
 
     public function removeEvent(Event $event): self
     {
-        if ($this->events->removeElement($event)) {
-            // set the owning side to null (unless already changed)
-            if ($event->getImage() === $this) {
-                $event->setImage(null);
-            }
+        // set the owning side to null (unless already changed)
+        if ($this->events->removeElement($event) && $event->getImage() === $this) {
+            $event->setImage(null);
         }
 
         return $this;
@@ -663,11 +691,9 @@ class File
 
     public function removeActivityLogo(Activity $activityLogo): self
     {
-        if ($this->activityLogos->removeElement($activityLogo)) {
-            // set the owning side to null (unless already changed)
-            if ($activityLogo->getLogo() === $this) {
-                $activityLogo->setLogo(null);
-            }
+        // set the owning side to null (unless already changed)
+        if ($this->activityLogos->removeElement($activityLogo) && $activityLogo->getLogo() === $this) {
+            $activityLogo->setLogo(null);
         }
 
         return $this;
@@ -693,11 +719,9 @@ class File
 
     public function removeActivityImage(Activity $activityImage): self
     {
-        if ($this->activityImages->removeElement($activityImage)) {
-            // set the owning side to null (unless already changed)
-            if ($activityImage->getImageActivity() === $this) {
-                $activityImage->setImageActivity(null);
-            }
+        // set the owning side to null (unless already changed)
+        if ($this->activityImages->removeElement($activityImage) && $activityImage->getImageActivity() === $this) {
+            $activityImage->setImageActivity(null);
         }
 
         return $this;

+ 2 - 0
src/Enum/Core/FileStatusEnum.php

@@ -8,6 +8,8 @@ use MyCLabs\Enum\Enum;
  * Statuts des fichiers
  * @method static PENDING()
  * @method static READY()
+ * @method static DELETED()
+ * @method static ERROR()
  */
 class FileStatusEnum extends Enum
 {

+ 13 - 0
src/Enum/Core/FileTypeEnum.php

@@ -0,0 +1,13 @@
+<?php
+
+namespace App\Enum\Core;
+
+use MyCLabs\Enum\Enum;
+
+/**
+ * @method static LICENCE_CMF()
+ */
+class FileTypeEnum extends Enum
+{
+    private const LICENCE_CMF ='LICENCE_CMF';
+}

+ 3 - 6
src/Enum/Export/ExportFormatEnum.php

@@ -26,14 +26,11 @@ class ExportFormatEnum extends Enum
     ];
 
     /**
-     * @param  string $formatShortName
+     * @param string $formatShortName
      * @return string
      */
-    public static function getMimeType($formatShortName)
+    public static function getMimeType(string $formatShortName): string
     {
-        if (!isset(static::$mimeType[$formatShortName])) {
-            return "Unknown format ($formatShortName)";
-        }
-        return static::$mimeType[$formatShortName];
+        return static::$mimeType[$formatShortName] ?? "Unknown format ($formatShortName)";
     }
 }

+ 3 - 3
src/Service/Export/BaseExporter.php

@@ -11,7 +11,7 @@ use App\Repository\Access\AccessRepository;
 use App\Repository\Core\FileRepository;
 use App\Service\Export\Model\ExportModelInterface;
 use App\Service\ServiceIterator\EncoderIterator;
-use App\Service\Storage\TemporaryFileStorage;
+use App\Service\Storage\FileStorage;
 use App\Service\Utils\StringsUtils;
 use Doctrine\ORM\EntityManagerInterface;
 use Exception;
@@ -30,7 +30,7 @@ abstract class BaseExporter implements ExporterInterface
     protected Environment $twig;
     protected EncoderIterator $encoderIterator;
     protected EntityManagerInterface $entityManager;
-    protected TemporaryFileStorage $storage;
+    protected FileStorage $storage;
     protected LoggerInterface $logger;
 
     #[Required]
@@ -49,7 +49,7 @@ abstract class BaseExporter implements ExporterInterface
     public function setEntityManager(EntityManagerInterface $entityManager): void
     { $this->entityManager = $entityManager; }
     #[Required]
-    public function setStorage(TemporaryFileStorage $storage): void
+    public function setStorage(FileStorage $storage): void
     { $this->storage = $storage; }
     #[Required]
     public function setLogger(LoggerInterface $logger): void

+ 4 - 3
src/Service/Export/LicenceCmfExporter.php

@@ -10,6 +10,7 @@ use App\Service\Export\Model\LicenceCmf;
 use App\Enum\Access\FunctionEnum;
 use App\Repository\Organization\OrganizationRepository;
 use App\Service\Export\Model\LicenceCmfCollection;
+use App\Service\Storage\FileStorage;
 use App\Service\Storage\UploadStorage;
 
 /**
@@ -27,7 +28,7 @@ class LicenceCmfExporter extends BaseExporter
 
     public function __construct(
         private OrganizationRepository $organizationRepository,
-        private UploadStorage $uploadStorage,
+        private FileStorage $fileStorage,
     )
     {}
 
@@ -62,7 +63,7 @@ class LicenceCmfExporter extends BaseExporter
         $logoId = $organization->getLogo()?->getId();
         if ($logoId) {
             $licenceCmf->setLogoUri(
-                $this->uploadStorage->getUri($logoId)
+                $this->fileStorage->getUri($logoId)
             );
         }
 
@@ -80,7 +81,7 @@ class LicenceCmfExporter extends BaseExporter
         $qrCodeId = $cmf->getParameters()?->getQrCode()?->getId();
         if ($qrCodeId) {
             $licenceCmf->setQrCodeUri(
-                $this->uploadStorage->getUri($qrCodeId)
+                $this->fileStorage->getUri($qrCodeId)
             );
         }
 

+ 276 - 6
src/Service/Storage/FileStorage.php

@@ -3,20 +3,290 @@ declare(strict_types=1);
 
 namespace App\Service\Storage;
 
+use App\Entity\Access\Access;
+use App\Entity\Core\File;
+use App\Entity\Organization\Organization;
+use App\Entity\Person\Person;
+use App\Enum\Core\FileStatusEnum;
+use App\Enum\Core\FileTypeEnum;
 use App\Service\Utils\Path;
+use DateTime;
+use Doctrine\ORM\EntityManagerInterface;
+use Exception;
+use Gaufrette\Filesystem;
 use Knp\Bundle\GaufretteBundle\FilesystemMap;
+use Mimey\MimeTypes;
+use Ramsey\Uuid\Uuid;
+use RuntimeException;
 
 /**
- * Base class for file storage
+ * Read and write files into the file storage
  */
-abstract class FileStorage
+class FileStorage
 {
+    /**
+     * Key of the gaufrette storage, as defined in config/packages/knp_gaufrette.yaml
+     */
+    protected const FS_KEY = 'storage';
+
+    protected Filesystem $filesystem;
+
     public function __construct(
-        protected FilesystemMap $filesystem
+        protected FilesystemMap $filesystemMap,
+        protected EntityManagerInterface $entityManager
     )
-    {}
+    {
+        $this->filesystem = $filesystemMap->get(static::FS_KEY);
+    }
+
+    /**
+     * Return true if the file exists in the file storage
+     * 
+     * @param File $file
+     * @return bool
+     */
+    public function exists(File $file): bool {
+        return $this->filesystem->has($file->getSlug());
+    }
+
+    /**
+     * Lists all the non-temporary files of the given owner
+     *
+     * @param Organization|Access|Person $owner
+     * @param FileTypeEnum|null $type
+     * @return array
+     */
+    public function listByOwner (
+        Organization | Access | Person $owner,
+        ?FileTypeEnum $type = null
+    ): array {
+        return $this->filesystem->listKeys(
+            $this->getPrefix($owner, false, $type?->getValue())
+        );
+    }
+
+    /**
+     * Reads the given file and returns its content as a string
+     *
+     * @param File $file
+     * @return string
+     */
+    public function read(File $file): string
+    {
+        return $this->filesystem->read($file->getSlug());
+    }
+
+    /**
+     * Prepare a File record with a PENDING status.
+     * This record will hold all the data needed to create the file, except its content.
+     *
+     * @param Organization|Access|Person $owner Owner of the file, either an organization, a person or both (access)
+     * @param string $filename The file's name (mandatory)
+     * @param FileTypeEnum $type The type of the new file
+     * @param Access $createdBy Id of the access responsible for this creation
+     * @param bool $isTemporary Is it a temporary file that can be deleted after some time
+     * @param string|null $mimeType Mimetype of the file, if not provided, the method will try to guess it from its file name's extension
+     * @param string $visibility
+     * @param bool $flushFile Should the newly created file be flushed after having been persisted?
+     * @return File
+     */
+    public function prepareFile(
+        Organization | Access | Person $owner,
+        string $filename,
+        FileTypeEnum $type,
+        Access $createdBy,
+        bool $isTemporary = false,
+        string $visibility = 'NOBODY',
+        string $mimeType = null,
+        bool $flushFile = true
+    ): File
+    {
+        $file = (new File())
+            ->setName($filename)
+            ->setSlug(null)
+            ->setType($type->getValue())
+            ->setVisibility($visibility)
+            ->setIsTemporaryFile($isTemporary)
+            ->setMimeType($mimeType ?? $this->guessMimeTypeFromFilename($filename))
+            ->setCreatedBy($createdBy->getId())
+            ->setStatus(FileStatusEnum::PENDING()->getValue());
+
+        if ($owner instanceof Access) {
+            $file->setOrganization($owner->getOrganization())
+                ->setPerson($owner->getPerson());
+        } else if ($owner instanceof Organization) {
+            $file->setOrganization($owner);
+        } else if ($owner instanceof Person) {
+            $file->setPerson($owner);
+        }
+
+        $this->entityManager->persist($file);
+
+        if ($flushFile) {
+            $this->entityManager->flush();
+        }
+
+        return $file;
+    }
+
+    /**
+     * Write the $content into the file storage and update the given File object's size, slug, status (READY)...
+     *
+     * @param File $file The file object that is about to be written
+     * @param string $content The content of the file
+     * @param Access $author The access responsible for the creation / update of the file
+     * @return File
+     */
+    public function writeFile(File $file, string $content, Access $author): File
+    {
+        if (empty($file->getName())) {
+            throw new RuntimeException('File has no filename');
+        }
+
+        /** @noinspection ProperNullCoalescingOperatorUsageInspection */
+        $prefix = $this->getPrefix(
+            $file->getOrganization() ?? $file->getPerson(),
+            $file->getIsTemporaryFile(),
+            $file->getType()
+        );
+
+        try {
+            $uid = date('Ymd_His') . '_' . substr(Uuid::uuid4()->toString(), 0, 5);
+        } catch (Exception $e) {
+            throw new RuntimeException('Error while generating the uuid', 0, $e);
+        }
+
+        $isNew = $file->getSlug() === null;
+        $key = $file->getSlug() ?? Path::join($prefix, $uid, $file->getName());
+
+        if (!$isNew && !$this->filesystem->has($key)) {
+            throw new RuntimeException('The file `' . $key . '` does not exist in the file storage');
+        }
+
+        $size = $this->filesystem->write($key, $content, true);
+
+        $file->setSize($size)
+            ->setStatus(FileStatusEnum::READY()->getValue());
+
+        if ($isNew) {
+            $file->setSlug($key)
+                 ->setCreateDate(new DateTime())
+                 ->setCreatedBy($author->getId());
+        } else {
+            $file->setUpdateDate(new DateTime())
+                 ->setUpdatedBy($author->getId());
+        }
+
+        $this->entityManager->flush();
+
+        return $file;
+    }
+
+    /**
+     * Convenient method to successively prepare and write a file
+     *
+     * @param Organization|Access|Person $owner
+     * @param string $filename
+     * @param FileTypeEnum $type
+     * @param string $content
+     * @param Access $author
+     * @param bool $isTemporary
+     * @param string|null $mimeType
+     * @param string $visibility
+     * @return File
+     */
+    public function makeFile (
+        Organization | Access | Person $owner,
+        string                         $filename,
+        FileTypeEnum                   $type,
+        string                         $content,
+        Access                         $author,
+        bool                           $isTemporary = false,
+        string                         $mimeType = null,
+        string                         $visibility = 'NOBODY'
+    ): File
+    {
+        $file = $this->prepareFile(
+            $owner,
+            $filename,
+            $type,
+            $author,
+            $isTemporary,
+            $mimeType,
+            $visibility,
+            false
+        );
+
+        return $this->writeFile($file, $content, $author);
+    }
+
+    /**
+     * Delete the given file from the filesystem and update the status of the File
+     *
+     * @param File $file
+     * @param Access $author
+     * @return File
+     */
+    public function delete(File $file, Access $author): File
+    {
+        $deleted = $this->filesystem->delete($file->getSlug());
+
+        if (!$deleted) {
+            throw new RuntimeException('File `' . $file->getSlug() . '` could\'nt be deleted');
+        }
+
+        $file->setStatus(FileStatusEnum::DELETED()->getValue())
+             ->setSize(0)
+             ->setUpdatedBy($author->getId());
+
+        return $file;
+    }
+
+    /**
+     * If an organization or access owns the file, the prefix will be '(_temp_/)organization/{id}'.
+     * If a person owns it, the prefix will be '{temp}/person/{id}'
+     *
+     * With {id} being the id of the organization or of the person.
+     *
+     * If the file is temporary, '_temp_/' is prepended to the prefix.
+     *
+     * @param Organization|Access|Person $owner
+     * @param bool $isTemporary
+     * @param string|null $type
+     * @return string
+     */
+    private function getPrefix(Organization | Access | Person $owner, bool $isTemporary, string $type = null): string
+    {
+        if ($owner instanceof Access) {
+            $prefix = Path::join('organization', $owner->getOrganization()?->getId(), $owner->getPerson()?->getId());
+        } else {
+            $prefix = Path::join($owner instanceof Person ? 'person' : 'organization', $owner->getId());
+        }
+
+        if ($isTemporary) {
+            $prefix = Path::join('_temp_', $prefix);
+        }
+
+        if ($type !== null) {
+            $prefix = Path::join($prefix, strtolower($type));
+        }
+
+        return $prefix;
+    }
 
-    protected function getStorageBaseDir(): string {
-        return Path::join(Path::getProjectDir(), 'var', 'files');
+    /**
+     * Try to guess the mimetype from the filename
+     *
+     * Return null if it did not manage to guess it.
+     *
+     * @param string $filename
+     * @return string|null
+     */
+    private function guessMimeTypeFromFilename(string $filename): string | null {
+        $ext = pathinfo($filename, PATHINFO_EXTENSION);
+        if (empty($ext)) {
+            return null;
+        }
+        return (new MimeTypes)->getMimeType($ext);
     }
 }

+ 0 - 49
src/Service/Storage/TemporaryFileStorage.php

@@ -1,49 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Service\Storage;
-
-use App\Service\Utils\Path;
-use Exception;
-use Gaufrette\Filesystem;
-use Knp\Bundle\GaufretteBundle\FilesystemMap;
-use Ramsey\Uuid\Uuid;
-
-/**
- * Gère le stockage des fichiers temporaires, comme les documents générés par les utilisateurs
- * comme des fichiers d'export
- */
-class TemporaryFileStorage
-{
-    private const TEMPDIR_RELPATH = 'temp';
-
-    private Filesystem $filesystem;
-
-    public function __construct(
-        FilesystemMap $filesystemMap
-    )
-    {
-        $this->filesystem = $filesystemMap->get('temp');
-    }
-
-    /**
-     * Write the given content to a temporary file
-     *
-     * @param string $filename
-     * @param string $content
-     * @return string
-     * @throws Exception
-     */
-    public function write(string $filename, string $content): string
-    {
-        // Temp dir name is a concatenation of current time (for convenience and sorting) and a short uuid4
-        $fileKey = Path::join(
-            date('Ymd_His') . '_' . substr(Uuid::uuid4()->toString(), 0, 8),
-            $filename
-        );
-        
-        $this->filesystem->getAdapter()->write($fileKey, $content);
-
-        return Path::join(self::TEMPDIR_RELPATH, $fileKey);
-    }
-}

+ 0 - 31
src/Service/Storage/UploadStorage.php

@@ -1,31 +0,0 @@
-<?php
-declare(strict_types=1);
-
-namespace App\Service\Storage;
-
-/**
- * Gère l'upload et le téléchargement de fichiers par les utilisateurs
- *
- * Pour la durée de la migration vers Symfony 5, la gestion des fichiers est déléguée à l'ancienne API
- *
- */
-// TODO: revoir le fonctionnement de ce storage pour le mettre sur le même format que les autres
-class UploadStorage
-{
-    public function __construct(private string $internalFilesUploadUri)
-    {}
-
-    private function getBaseDownloadUri(): string {
-        return $this->internalFilesUploadUri;
-    }
-
-    private static function getUploadUri(): string
-    {
-        return '';
-    }
-
-    public function getUri(int $fileId): string {
-        return rtrim(self::getBaseDownloadUri(), '/') . '/' . $fileId;
-    }
-
-}