vendor/boldr/cms-bundle/src/Upload/UploadStorageManager.php line 335

Open in your IDE?
  1. <?php
  2. namespace Boldr\Cms\CmsBundle\Upload;
  3. use Boldr\Cms\CmsBundle\Entity\Upload;
  4. use Boldr\Cms\CmsBundle\Storage\StorageBucketInterface;
  5. use Boldr\Cms\CmsBundle\Storage\StorageBucketFactoryInterface;
  6. use Symfony\Component\HttpFoundation\{ Response, BinaryFileResponse };
  7. use Symfony\Component\HttpFoundation\HeaderUtils;
  8. use Doctrine\ORM\EntityManagerInterface;
  9. use Symfony\Component\Mime\MimeTypesInterface;
  10. use Exception;
  11. class UploadStorageManager
  12. {
  13. private array $mediaSizes;
  14. /**
  15. * Storage for "raw" uploaded files
  16. */
  17. private StorageBucketInterface $uploadsStorageBucket;
  18. /**
  19. * Storage for resized, reformatted files
  20. */
  21. private StorageBucketFactoryInterface $derivativesStorageBucketFactory;
  22. private EntityManagerInterface $entityManager;
  23. private ImageFormatter $imageFormatter;
  24. private MimeTypesInterface $mimeTypes;
  25. public function __construct(
  26. array $mediaSizes,
  27. StorageBucketInterface $uploadsStorageBucket,
  28. StorageBucketFactoryInterface $derivativesStorageBucketFactory,
  29. EntityManagerInterface $entityManager,
  30. ImageFormatter $imageFormatter,
  31. MimeTypesInterface $mimeTypes
  32. )
  33. {
  34. $this->mediaSizes = $mediaSizes;
  35. $this->uploadsStorageBucket = $uploadsStorageBucket;
  36. $this->derivativesStorageBucketFactory = $derivativesStorageBucketFactory;
  37. $this->entityManager = $entityManager;
  38. $this->imageFormatter = $imageFormatter;
  39. $this->mimeTypes = $mimeTypes;
  40. }
  41. /**
  42. * Try to upload a file.
  43. */
  44. public function upload(string $file, string $preferredPublicName): ?Upload
  45. {
  46. $storageIdentifier = $this->uploadsStorageBucket->store($file, $preferredPublicName);
  47. if ($storageIdentifier === null)
  48. {
  49. return null;
  50. }
  51. $upload = new Upload;
  52. $upload->setMimeType($this->mimeTypes->getMimeTypes(pathinfo($preferredPublicName, PATHINFO_EXTENSION))[0] ?? 'application/octet-stream');
  53. $upload->setStorageIdentifier($storageIdentifier);
  54. $upload->setPublicName($preferredPublicName);
  55. $upload->setSize(filesize($file));
  56. if (strpos($upload->getMimeType(), 'image/') === 0)
  57. {
  58. $upload->setType(Upload::TYPE_IMAGE);
  59. if ($upload->getMimeType() !== 'image/svg+xml')
  60. {
  61. [$width, $height] = getimagesize($file);
  62. $upload->setWidth($width);
  63. $upload->setHeight($height);
  64. }
  65. }
  66. if (strpos($upload->getMimeType(), 'audio/') === 0)
  67. {
  68. $upload->setType(Upload::TYPE_AUDIO);
  69. }
  70. if (strpos($upload->getMimeType(), 'video/') === 0)
  71. {
  72. $upload->setType(Upload::TYPE_VIDEO);
  73. }
  74. return $upload;
  75. }
  76. /**
  77. * Creates a response that will download the file.
  78. */
  79. public function download(Upload $upload, ?string $name = null): Response
  80. {
  81. $storageIdentifier = $upload->getStorageIdentifier();
  82. $localPath = $this->uploadsStorageBucket->getLocalPath($storageIdentifier);
  83. $isTempFile = false;
  84. if (!$localPath)
  85. {
  86. $localPath = tempnam(sys_get_temp_dir(), 'tmpdownload');
  87. $isTempFile = true;
  88. $this->uploadsStorageBucket->copy($storageIdentifier, $localPath);
  89. }
  90. $response = new BinaryFileResponse($localPath);
  91. $response->deleteFileAfterSend($isTempFile);
  92. $disposition = HeaderUtils::makeDisposition(
  93. HeaderUtils::DISPOSITION_ATTACHMENT,
  94. $name ?? $upload->getPublicName()
  95. );
  96. $response->headers->set('Content-Disposition', $disposition);
  97. return $response;
  98. }
  99. /**
  100. * Try to delete an upload.
  101. */
  102. public function delete(Upload $upload): void
  103. {
  104. $this->uploadsStorageBucket->delete($upload->getStorageIdentifier());
  105. foreach ($upload->getDerivatives() as $bucketName => $identifier)
  106. {
  107. $bucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
  108. $bucket->delete($identifier);
  109. }
  110. $this->entityManager->remove($upload);
  111. }
  112. public function setPublicName(Upload $upload, string $publicName): void
  113. {
  114. $upload->setPublicName($publicName);
  115. $newIdentifier = $this->uploadsStorageBucket->setPublicName($upload->getStorageIdentifier(), $publicName);
  116. if ($newIdentifier !== null)
  117. {
  118. $upload->setStorageIdentifier($newIdentifier);
  119. }
  120. $newDerivatives = [];
  121. foreach ($upload->getDerivatives() as $bucketName => $identifier)
  122. {
  123. $bucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
  124. $newIdentifier = $bucket->setPublicName($identifier, $publicName);
  125. $newDerivatives[$bucketName] = $newIdentifier ?? $identifier;
  126. }
  127. $upload->setDerivatives($newDerivatives);
  128. }
  129. /**
  130. * @param string|null $format Format, or NULL if original format
  131. */
  132. public function getImageUrl(Upload $upload, string $baseUrl, ?string $size = null, ?string $format = null, bool $regenerate = false): string
  133. {
  134. if ($upload->getType() !== Upload::TYPE_IMAGE)
  135. {
  136. throw new \Exception('UploadStorageManager::getImageUrl: Argument #1 ($upload) must be an Image upload.');
  137. }
  138. // Generate derivative and get bucket name
  139. $bucketName = $this->generateDerivative($upload, $size, $format, $regenerate);
  140. // Retrieve public URL from the appropriate bucket
  141. $storageBucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
  142. return $storageBucket->getPublicUrl($upload->getDerivatives()[$bucketName], $baseUrl);
  143. }
  144. public function getUploadPath(Upload $upload): string
  145. {
  146. $localPath = $this->uploadsStorageBucket->getLocalPath($upload->getStorageIdentifier());
  147. if ($localPath === null)
  148. {
  149. $localPath = tempnam(sys_get_temp_dir(), 'boldrcms');
  150. $this->uploadsStorageBucket->copy($upload->getStorageIdentifier(), $localPath);
  151. }
  152. return $localPath;
  153. }
  154. public function getUploadContents(Upload $upload): string
  155. {
  156. return $this->uploadsStorageBucket->getContents($upload->getStorageIdentifier());
  157. }
  158. /**
  159. * @param string|null $format Format, or NULL if original format
  160. */
  161. public function getImageContents(Upload $upload, string $baseUrl, ?string $size = null, ?string $format = null, bool $regenerate = false): string
  162. {
  163. if ($upload->getType() !== Upload::TYPE_IMAGE)
  164. {
  165. throw new \Exception('UploadStorageManager::getImageUrl: Argument #1 ($upload) must be an Image upload.');
  166. }
  167. // Generate derivative and get bucket name
  168. $bucketName = $this->generateDerivative($upload, $size, $format, $regenerate);
  169. // Retrieve public URL from the appropriate bucket
  170. $storageBucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
  171. return $storageBucket->getContents($upload->getDerivatives()[$bucketName], $baseUrl);
  172. }
  173. /**
  174. * @param string|null $format Format, or NULL if original format
  175. */
  176. public function getImagePath(Upload $upload, string $baseUrl, ?string $size = null, ?string $format = null, bool $regenerate = false): string
  177. {
  178. if ($upload->getType() !== Upload::TYPE_IMAGE)
  179. {
  180. throw new \Exception('UploadStorageManager::getImageUrl: Argument #1 ($upload) must be an Image upload.');
  181. }
  182. // Generate derivative and get bucket name
  183. $bucketName = $this->generateDerivative($upload, $size, $format, $regenerate);
  184. // Retrieve public URL from the appropriate bucket
  185. $storageBucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
  186. $localPath = $storageBucket->getLocalPath($upload->getDerivatives()[$bucketName]);
  187. if ($localPath === null)
  188. {
  189. $localPath = tempnam(sys_get_temp_dir(), 'boldrcms');
  190. $storageBucket->copy($upload->getDerivatives()[$bucketName], $localPath);
  191. }
  192. return $localPath;
  193. }
  194. /**
  195. * @param string|null $format Format, or NULL if original format
  196. */
  197. public function getPublicUrl(Upload $upload, string $baseUrl): string
  198. {
  199. // Retrieve public URL from the appropriate bucket
  200. $bucketName = match ($upload->getType()) {
  201. Upload::TYPE_IMAGE => 'image',
  202. Upload::TYPE_VIDEO => 'video',
  203. Upload::TYPE_AUDIO => 'audio',
  204. Upload::TYPE_DOCUMENT => 'document'
  205. };
  206. $storageBucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
  207. if (!isset($upload->getDerivatives()[$bucketName]))
  208. {
  209. $localPath = $this->uploadsStorageBucket->getLocalPath($upload->getStorageIdentifier());
  210. if ($localPath !== null)
  211. {
  212. $identifier = $storageBucket->store($localPath, $upload->getPublicName());
  213. }
  214. else
  215. {
  216. $contents = $this->uploadsStorageBucket->getContents($upload->getStorageIdentifier());
  217. $identifier = $storageBucket->storeContent($contents, $upload->getPublicName());
  218. }
  219. $upload->addDerivative($bucketName, $identifier);
  220. $this->entityManager->flush();
  221. }
  222. return $storageBucket->getPublicUrl($upload->getDerivatives()[$bucketName], $baseUrl);
  223. }
  224. public function getImageBucketName(Upload $upload, ?string $size = null, ?string $format = null): ?string
  225. {
  226. if ($upload->getType() !== Upload::TYPE_IMAGE)
  227. {
  228. return 'public';
  229. }
  230. if ($format !== null && !strpos($format, 'image/') === 0)
  231. {
  232. throw new \Exception('Cannot convert image upload to non-image mime type.');
  233. }
  234. $originalFormat = explode('/', $upload->getMimeType(), 2)[1];
  235. $originalFormat = explode('+', $originalFormat, 2)[0];
  236. // SVG only comes in 1 size and cannot be converted to a different format
  237. if ($originalFormat === 'svg')
  238. {
  239. return 'svg';
  240. }
  241. $newFormat = $format === null ? $originalFormat : substr($format, 6);
  242. if ($size === null)
  243. {
  244. $size = 'full';
  245. }
  246. return $size . '/'. $newFormat;
  247. }
  248. /**
  249. * Enqueue upload for derivative generation.
  250. *
  251. * @return bool Whether the derivative was enqueued
  252. */
  253. public function enqueueDerivative(Upload $upload, ?string $size = null, ?string $format = null): bool
  254. {
  255. if ($upload->getMimeType() === 'image/svg+xml')
  256. {
  257. return false;
  258. }
  259. // Find the appropriate bucket name
  260. $bucketName = $this->getImageBucketName($upload, $size, $format);
  261. if ($bucketName === null)
  262. {
  263. return false;
  264. }
  265. if (isset($upload->getDerivatives()[$bucketName]))
  266. {
  267. return false;
  268. }
  269. $upload->enqueueDerivative($size, $format);
  270. return true;
  271. }
  272. /**
  273. * @param string|null $format Format, or NULL if original format
  274. */
  275. public function generateDerivative(Upload $upload, ?string $size = null, ?string $format = null, bool $regenerate = false, bool &$alreadyExisted = null): string
  276. {
  277. // SVGs get special treatment: they are stored in a bucket "svg" and not resized, etc.
  278. $isSvg = $upload->getMimeType() === 'image/svg+xml';
  279. // Find the appropriate bucket name
  280. $bucketName = $this->getImageBucketName($upload, $size, $format);
  281. if ($bucketName === null)
  282. {
  283. throw new \Exception('Cannot generate derivative: no appropriate storage bucket.');
  284. }
  285. // Check if the derivative already exists
  286. $derivatives = $upload->getDerivatives();
  287. $alreadyExisted = isset($derivatives[$bucketName]);
  288. // If the derivative does not exist yet, generate it
  289. if (!$alreadyExisted || $regenerate)
  290. {
  291. $storageBucket = $this->derivativesStorageBucketFactory->getBucket($bucketName);
  292. $identifier = $upload->getStorageIdentifier();
  293. // If it is an SVG, copy the content to another storage
  294. $sourceFile = $this->uploadsStorageBucket->getLocalPath($identifier);
  295. if ($isSvg)
  296. {
  297. if ($sourceFile !== null)
  298. {
  299. $identifier = $storageBucket->store($sourceFile, $upload->getPublicName());
  300. }
  301. else
  302. {
  303. $contents = $this->uploadsStorageBucket->getContents($identifier);
  304. $identifier = $storageBucket->storeContent($contents, $upload->getPublicName());
  305. }
  306. }
  307. else
  308. {
  309. $removeSourceFile = false;
  310. if ($sourceFile === null)
  311. {
  312. $sourceFile = tempnam(sys_get_temp_dir(), 'bcmsimage');
  313. $removeSourceFile = true;
  314. $success = $this->uploadsStorageBucket->copy($identifier, $sourceFile);
  315. if (!$success)
  316. {
  317. throw new \Exception('Could not copy source image "'. $identifier . '" from storage for formatting.');
  318. }
  319. }
  320. // Format image
  321. $temporaryFile = tempnam(sys_get_temp_dir(), 'bcmsimage');
  322. $imageFormatterOptions = $size === null ? null : $this->getImageFormatterOptions($size);
  323. $extension = $this->imageFormatter->format($sourceFile, $temporaryFile, $imageFormatterOptions, $format);
  324. $publicName = pathinfo($upload->getPublicName(), PATHINFO_FILENAME) .'.'. $extension;
  325. // Store derivative on file storage
  326. $identifier = $storageBucket->store($temporaryFile, $publicName);
  327. @unlink($temporaryFile);
  328. if ($removeSourceFile)
  329. {
  330. @unlink($sourceFile);
  331. }
  332. }
  333. // Save identifier of derivative file in Upload entity
  334. $derivatives[$bucketName] = $identifier;
  335. $upload->setDerivatives($derivatives);
  336. $this->entityManager->flush();
  337. }
  338. return $bucketName;
  339. }
  340. /**
  341. * Gets the size of an image
  342. */
  343. public function getImageDimensions(Upload $upload): array
  344. {
  345. if ($upload->getType() !== Upload::TYPE_IMAGE)
  346. {
  347. throw new \Exception('Upload is not an image');
  348. }
  349. $localPath = $this->uploadsStorageBucket->getLocalPath($upload->getStorageIdentifier());
  350. if ($localPath !== null)
  351. {
  352. [$width, $height] = getimagesize($localPath);
  353. return [$width, $height];
  354. }
  355. $temporaryFile = tempnam(sys_get_temp_dir(), 'bcmsimage');
  356. $this->uploadsStorageBucket->copy($upload->getStorageIdentifier(), $temporaryFile);
  357. [$width, $height] = getimagesize($temporaryFile);
  358. unlink($temporaryFile);
  359. return [$width, $height];
  360. }
  361. private function getImageFormatterOptions(string $sizeName): ImageFormatterOptions
  362. {
  363. if ($sizeName === 'boldr_cms_attachment_selector_thumbnail')
  364. {
  365. $sizeName = 'boldr_cms_upload_selector_thumbnail';
  366. }
  367. if ($sizeName === 'boldr_cms_attachment_selector_preview')
  368. {
  369. $sizeName = 'boldr_cms_upload_selector_preview';
  370. }
  371. if (!isset($this->mediaSizes[$sizeName]))
  372. {
  373. throw new \Exception('Unknown media size "'.$sizeName.'"');
  374. }
  375. $size = $this->mediaSizes[$sizeName];
  376. $imageFormatterOptions = new ImageFormatterOptions($size['width'], $size['height']);
  377. $imageFormatterOptions->setBehavior($size['behavior']);
  378. if (isset($size['background']))
  379. {
  380. $imageFormatterOptions->setBackgroundColor($size['background']);
  381. }
  382. if (isset($size['overlay']))
  383. {
  384. $imageFormatterOptions->setOverlays([$size['overlay']]);
  385. }
  386. return $imageFormatterOptions;
  387. }
  388. }