LocalStorage.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Service\File\Storage;
  4. use App\Entity\Access\Access;
  5. use App\Entity\Core\File;
  6. use App\Entity\Organization\Organization;
  7. use App\Entity\Person\Person;
  8. use App\Enum\Core\FileStatusEnum;
  9. use App\Enum\Core\FileTypeEnum;
  10. use App\Repository\Access\AccessRepository;
  11. use App\Service\File\FileManager;
  12. use App\Service\Utils\Path;
  13. use App\Service\Utils\Uuid;
  14. use DateTime;
  15. use Doctrine\ORM\EntityManagerInterface;
  16. use Gaufrette\FilesystemInterface;
  17. use JetBrains\PhpStorm\Pure;
  18. use Knp\Bundle\GaufretteBundle\FilesystemMap;
  19. use RuntimeException;
  20. /**
  21. * Read and write files into the file storage
  22. */
  23. class LocalStorage implements FileStorageInterface
  24. {
  25. /**
  26. * Key of the gaufrette storage, as defined in config/packages/knp_gaufrette.yaml
  27. */
  28. protected const FS_KEY = 'storage';
  29. protected FilesystemInterface $filesystem;
  30. public function __construct(
  31. protected FilesystemMap $filesystemMap,
  32. protected EntityManagerInterface $entityManager,
  33. protected AccessRepository $accessRepository
  34. )
  35. {
  36. $this->filesystem = $filesystemMap->get(static::FS_KEY);
  37. }
  38. /**
  39. * Return true if the file exists in the file storage
  40. *
  41. * @param File $file
  42. * @return bool
  43. */
  44. public function exists(File $file): bool
  45. {
  46. return $this->filesystem->has($file->getSlug());
  47. }
  48. /**
  49. * Lists all the non-temporary files of the given owner
  50. *
  51. * @param Organization|Access|Person $owner
  52. * @param FileTypeEnum|null $type
  53. * @return list<string>
  54. */
  55. public function listByOwner (
  56. Organization | Access | Person $owner,
  57. ?FileTypeEnum $type = null
  58. ): array {
  59. return $this->filesystem->listKeys(
  60. $this->getPrefix($owner, false, $type?->getValue())
  61. );
  62. }
  63. /**
  64. * Reads the given file and returns its content as a string
  65. *
  66. * @param File $file
  67. * @return string
  68. */
  69. public function read(File $file): string
  70. {
  71. return $this->filesystem->read($file->getSlug());
  72. }
  73. /**
  74. * Prepare a File record with a PENDING status.
  75. * This record will hold all the data needed to create the file, except its content.
  76. *
  77. * @param Organization|Access|Person $owner Owner of the file, either an organization, a person or both (access)
  78. * @param string $filename The file's name (mandatory)
  79. * @param FileTypeEnum $type The type of the new file
  80. * @param Access $createdBy Id of the access responsible for this creation
  81. * @param bool $isTemporary Is it a temporary file that can be deleted after some time
  82. * @param string|null $mimeType Mimetype of the file, if not provided, the method will try to guess it from its file name's extension
  83. * @param string $visibility
  84. * @param bool $flushFile Should the newly created file be flushed after having been persisted?
  85. * @return File
  86. */
  87. public function prepareFile(
  88. Organization | Access | Person $owner,
  89. string $filename,
  90. FileTypeEnum $type,
  91. Access $createdBy,
  92. bool $isTemporary = false,
  93. string $visibility = 'NOBODY',
  94. string $mimeType = null,
  95. bool $flushFile = true
  96. ): File
  97. {
  98. [$organization, $person] = $this->getOrganizationAndPersonFromOwner($owner);
  99. $file = (new File())
  100. ->setName($filename)
  101. ->setOrganization($organization)
  102. ->setPerson($person)
  103. ->setSlug(null)
  104. ->setType($type->getValue())
  105. ->setVisibility($visibility)
  106. ->setIsTemporaryFile($isTemporary)
  107. ->setMimeType($mimeType ?? FileManager::guessMimeTypeFromFilename($filename))
  108. ->setCreateDate(new DateTime())
  109. ->setCreatedBy($createdBy->getId())
  110. ->setStatus(FileStatusEnum::PENDING()->getValue());
  111. $this->entityManager->persist($file);
  112. if ($flushFile) {
  113. $this->entityManager->flush();
  114. }
  115. return $file;
  116. }
  117. /**
  118. * Write the $content into the file storage and update the given File object's size, slug, status (READY)...
  119. *
  120. * @param File $file The file object that is about to be written
  121. * @param string $content The content of the file
  122. * @param Access $author The access responsible for the creation / update of the file
  123. * @return File
  124. */
  125. public function write(File $file, string $content, Access $author): File
  126. {
  127. if (empty($file->getName())) {
  128. throw new RuntimeException('File has no filename');
  129. }
  130. $isNewFile = $file->getSlug() === null;
  131. if ($isNewFile) {
  132. // Try to get the Access owner from the organization_id and person_id
  133. $access = null;
  134. if ($file->getOrganization() !== null && $file->getPerson() !== null) {
  135. $access = $this->accessRepository->findOneBy(
  136. ['organization' => $file->getOrganization(), 'person' => $file->getPerson()]
  137. );
  138. }
  139. $prefix = $this->getPrefix(
  140. $access ?? $file->getOrganization() ?? $file->getPerson(),
  141. $file->getIsTemporaryFile(),
  142. $file->getType()
  143. );
  144. $uid = date('Ymd_His') . '_' . Uuid::uuid(5);
  145. $key = Path::join($prefix, $uid, $file->getName());
  146. } else {
  147. $key = $file->getSlug();
  148. }
  149. if (!$isNewFile && !$this->filesystem->has($key)) {
  150. throw new RuntimeException('The file `' . $key . '` does not exist in the file storage');
  151. }
  152. $size = $this->filesystem->write($key, $content, true);
  153. $file->setSize($size)
  154. ->setStatus(FileStatusEnum::READY()->getValue());
  155. if ($isNewFile) {
  156. $file->setSlug($key)
  157. ->setCreateDate(new DateTime())
  158. ->setCreatedBy($author->getId());
  159. } else {
  160. $file->setUpdateDate(new DateTime())
  161. ->setUpdatedBy($author->getId());
  162. }
  163. $this->entityManager->flush();
  164. return $file;
  165. }
  166. /**
  167. * Convenient method to successively prepare and write a file
  168. *
  169. * @param Organization|Access|Person $owner
  170. * @param string $filename
  171. * @param FileTypeEnum $type
  172. * @param string $content
  173. * @param Access $author
  174. * @param bool $isTemporary
  175. * @param string|null $mimeType
  176. * @param string $visibility
  177. * @param string|null $config
  178. * @return File
  179. */
  180. public function makeFile (
  181. Organization | Access | Person $owner,
  182. string $filename,
  183. FileTypeEnum $type,
  184. string $content,
  185. Access $author,
  186. bool $isTemporary = false,
  187. string $visibility = 'NOBODY',
  188. string $mimeType = null,
  189. string $config = null
  190. ): File
  191. {
  192. $file = $this->prepareFile(
  193. $owner,
  194. $filename,
  195. $type,
  196. $author,
  197. $isTemporary,
  198. $visibility,
  199. $mimeType,
  200. false
  201. );
  202. if (!empty($config)) {
  203. // TODO: Déplacer dans le prepareFile?
  204. $file->setConfig($config);
  205. }
  206. return $this->write($file, $content, $author);
  207. }
  208. /**
  209. * Uupdate the status of the File
  210. *
  211. * @param File $file
  212. * @param Access $author
  213. * @return File
  214. */
  215. public function softDelete(File $file, Access $author): File
  216. {
  217. $file->setStatus(FileStatusEnum::DELETED()->getValue())
  218. ->setSize(0)
  219. ->setUpdatedBy($author->getId());
  220. return $file;
  221. }
  222. /**
  223. * Delete the given file from the filesystem
  224. *
  225. * @param File $file
  226. */
  227. public function hardDelete(File $file): void
  228. {
  229. $deleted = $this->filesystem->delete($file->getSlug());
  230. if (!$deleted) {
  231. throw new RuntimeException('File `' . $file->getSlug() . '` could\'nt be deleted');
  232. }
  233. }
  234. /**
  235. * If an organization owns the file, the prefix will be '(_temp_/)organization/{id}(/{type})'.
  236. * If a person owns it, the prefix will be '(_temp_/)person/{id}(/{type})'
  237. * If access owns it, the prefix will be '(_temp_/)organization/{organization_id}/{access_id}(/{type})'
  238. *
  239. * With {id} being the id of the organization or of the person.
  240. *
  241. * If the file is temporary, '_temp_/' is prepended to the prefix.
  242. * If a file type is given, this type is appended to the prefix (low case)
  243. *
  244. * @param Organization|Access|Person $owner
  245. * @param bool $isTemporary
  246. * @param string|null $type
  247. * @return string
  248. */
  249. protected function getPrefix(
  250. Organization | Access | Person $owner,
  251. bool $isTemporary,
  252. string $type = null
  253. ): string
  254. {
  255. if ($owner instanceof Access) {
  256. $prefix = Path::join('organization', $owner->getOrganization()?->getId(), $owner->getId());
  257. } else if ($owner instanceof Organization) {
  258. $prefix = Path::join('organization', $owner->getId());
  259. } else {
  260. $prefix = Path::join('person', $owner->getId());
  261. }
  262. if ($isTemporary) {
  263. $prefix = Path::join('temp', $prefix);
  264. }
  265. if ($type !== null && $type !== FileTypeEnum::NONE()->getValue()) {
  266. $prefix = Path::join($prefix, strtolower($type));
  267. }
  268. return $prefix;
  269. }
  270. /**
  271. * Return an array [$organization, $person] from a given owner
  272. *
  273. * @param Organization|Access|Person $owner
  274. * @return list<Organization | Person>
  275. */
  276. #[Pure]
  277. protected function getOrganizationAndPersonFromOwner(Organization | Access | Person $owner): array
  278. {
  279. if ($owner instanceof Access) {
  280. return [$owner->getOrganization(), $owner->getPerson()];
  281. }
  282. if ($owner instanceof Organization) {
  283. return [$owner, null];
  284. }
  285. return [null, $owner];
  286. }
  287. }