diff --git a/.env.test b/.env.test
index 93ae08c8..661ab23d 100644
--- a/.env.test
+++ b/.env.test
@@ -35,6 +35,7 @@ MESSENGER_DISTRIBUTION_TOPIC=anzu_core_dam_distribution
MESSENGER_DISTRIBUTION_REMOTE_PROCESSED_CHECK_TOPIC=anzu_core_dam_distribution_remote_check
MESSENGER_NOTIFICATION_TOPIC=notification_server_internal
MESSENGER_PROPERTY_REFRESH_TOPIC=anzu_core_dam_property_refresh
+MESSENGER_ASSET_COPY_TOPIC=anzu_core_dam_asset_copy
YOUTUBE_API_KEY=''
diff --git a/psalm.xml b/psalm.xml
index 4f2973e2..3efbcf6c 100644
--- a/psalm.xml
+++ b/psalm.xml
@@ -36,6 +36,7 @@
+
diff --git a/src/Controller/Api/Adm/V1/ImageController.php b/src/Controller/Api/Adm/V1/ImageController.php
index 9338f97d..292fe800 100644
--- a/src/Controller/Api/Adm/V1/ImageController.php
+++ b/src/Controller/Api/Adm/V1/ImageController.php
@@ -15,6 +15,7 @@
use AnzuSystems\CoreDamBundle\Domain\AssetFile\AssetFileDownloadFacade;
use AnzuSystems\CoreDamBundle\Domain\AssetFileRoute\AssetFileRouteFacade;
use AnzuSystems\CoreDamBundle\Domain\Chunk\ChunkFacade;
+use AnzuSystems\CoreDamBundle\Domain\Image\ImageCopyFacade;
use AnzuSystems\CoreDamBundle\Domain\Image\ImageFacade;
use AnzuSystems\CoreDamBundle\Domain\Image\ImagePositionFacade;
use AnzuSystems\CoreDamBundle\Domain\Image\ImageStatusFacade;
@@ -25,20 +26,26 @@
use AnzuSystems\CoreDamBundle\Exception\AssetSlotUsedException;
use AnzuSystems\CoreDamBundle\Exception\ForbiddenOperationException;
use AnzuSystems\CoreDamBundle\Exception\InvalidExtSystemConfigurationException;
+use AnzuSystems\CoreDamBundle\Model\Attributes\SerializeIterableParam;
use AnzuSystems\CoreDamBundle\Model\Dto\Asset\AssetAdmFinishDto;
use AnzuSystems\CoreDamBundle\Model\Dto\AssetExternalProvider\UploadAssetFromExternalProviderDto;
use AnzuSystems\CoreDamBundle\Model\Dto\AssetFileRoute\AssetFileRouteAdmDetailDecorator;
use AnzuSystems\CoreDamBundle\Model\Dto\Audio\AudioFileAdmDetailDto;
use AnzuSystems\CoreDamBundle\Model\Dto\Chunk\ChunkAdmCreateDto;
+use AnzuSystems\CoreDamBundle\Model\Dto\Image\AssetFileCopyResultDto;
use AnzuSystems\CoreDamBundle\Model\Dto\Image\ImageAdmCreateDto;
+use AnzuSystems\CoreDamBundle\Model\Dto\Image\ImageCopyDto;
use AnzuSystems\CoreDamBundle\Model\Dto\Image\ImageFileAdmDetailDto;
+use AnzuSystems\CoreDamBundle\Model\OpenApi\Request\OARequest as OADamRequest;
use AnzuSystems\CoreDamBundle\Security\Permission\DamPermissions;
use AnzuSystems\SerializerBundle\Attributes\SerializeParam;
use AnzuSystems\SerializerBundle\Exception\SerializerException;
+use Doctrine\Common\Collections\Collection;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
+use Throwable;
#[Route(path: '/image', name: 'adm_image_v1_')]
#[OA\Tag('Image')]
@@ -51,6 +58,7 @@ public function __construct(
private readonly AssetFileDownloadFacade $assetFileDownloadFacade,
private readonly ImagePositionFacade $imagePositionFacade,
private readonly AssetFileRouteFacade $routeFacade,
+ private readonly ImageCopyFacade $imageCopyFacade,
) {
}
@@ -273,6 +281,27 @@ public function generateDownloadUrl(ImageFile $image): JsonResponse
);
}
+ /**
+ * @param Collection $copyList
+ * @throws Throwable
+ *
+ * @throws ForbiddenOperationException
+ */
+ #[Route(
+ path: '/copy-to-licence',
+ name: 'copy_image',
+ methods: [Request::METHOD_PATCH]
+ )]
+ #[OADamRequest([ImageCopyDto::class]), OAResponse([AssetFileCopyResultDto::class]), OAResponseValidation]
+ public function copyToLicence(#[SerializeIterableParam(type: ImageCopyDto::class)] Collection $copyList): JsonResponse
+ {
+ $this->denyAccessUnlessGranted(DamPermissions::DAM_IMAGE_CREATE);
+
+ return $this->okResponse(
+ $this->imageCopyFacade->prepareCopyList($copyList)
+ );
+ }
+
/**
* @throws ForbiddenOperationException
*/
diff --git a/src/Controller/ImageController.php b/src/Controller/ImageController.php
index 25d35149..f564cae8 100644
--- a/src/Controller/ImageController.php
+++ b/src/Controller/ImageController.php
@@ -17,6 +17,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Routing\Requirement\Requirement;
#[Route(path: '/image', name: 'image_')]
final class ImageController extends AbstractImageController
@@ -77,7 +78,7 @@ public function animation(
path: '/{requestWidth}{requestHeight}{regionOfInterestId}{quality}/{imageId}.jpg',
name: 'get_one_file_name',
requirements: [
- 'imageId' => '[0-9a-zA-Z-]+',
+ 'imageId' => Requirement::UUID,
'requestWidth' => 'w\d+',
'requestHeight' => '-h\d+',
'regionOfInterestId' => '(-c\d+)|',
diff --git a/src/DataFixtures/AssetLicenceFixtures.php b/src/DataFixtures/AssetLicenceFixtures.php
index 12fa6b8a..9b90a35a 100644
--- a/src/DataFixtures/AssetLicenceFixtures.php
+++ b/src/DataFixtures/AssetLicenceFixtures.php
@@ -46,20 +46,19 @@ public function load(ProgressBar $progressBar): void
private function getData(): Generator
{
- $existingLicence = $this->assetLicenceRepository->find(self::DEFAULT_LICENCE_ID);
- if ($existingLicence) {
- return;
- }
-
/** @var ExtSystem $cmsExtSystem */
$cmsExtSystem = $this->entityManager->find(
ExtSystem::class,
1
);
- yield (new AssetLicence())
- ->setId(self::DEFAULT_LICENCE_ID)
- ->setExtId('1')
- ->setExtSystem($cmsExtSystem);
+ $existingLicence = $this->assetLicenceRepository->find(self::DEFAULT_LICENCE_ID);
+ if (null === $existingLicence) {
+ yield (new AssetLicence())
+ ->setId(self::DEFAULT_LICENCE_ID)
+ ->setExtId('1')
+ ->setExtSystem($cmsExtSystem);
+ }
+
}
}
diff --git a/src/DependencyInjection/AnzuSystemsCoreDamExtension.php b/src/DependencyInjection/AnzuSystemsCoreDamExtension.php
index 192db1a9..0901cd37 100644
--- a/src/DependencyInjection/AnzuSystemsCoreDamExtension.php
+++ b/src/DependencyInjection/AnzuSystemsCoreDamExtension.php
@@ -20,6 +20,7 @@
use AnzuSystems\CoreDamBundle\Messenger\Message\AssetFileMetadataProcessMessage;
use AnzuSystems\CoreDamBundle\Messenger\Message\AssetRefreshPropertiesMessage;
use AnzuSystems\CoreDamBundle\Messenger\Message\AudioFileChangeStateMessage;
+use AnzuSystems\CoreDamBundle\Messenger\Message\CopyAssetFileMessage;
use AnzuSystems\CoreDamBundle\Messenger\Message\DistributeMessage;
use AnzuSystems\CoreDamBundle\Messenger\Message\DistributionRemoteProcessingCheckMessage;
use AnzuSystems\CoreDamBundle\Messenger\Message\DocumentFileChangeStateMessage;
@@ -108,6 +109,8 @@ public function prepend(ContainerBuilder $container): void
$distributionRemoteProcessedCheckTopicDsn = "%env(MESSENGER_TRANSPORT_DSN)%/{$distributionRemoteProcessedCheckTopic}";
$assetPropertyRefreshTopic = '%env(MESSENGER_PROPERTY_REFRESH_TOPIC)%';
$assetPropertyRefreshTopicDsn = "%env(MESSENGER_TRANSPORT_DSN)%/{$assetPropertyRefreshTopic}";
+ $assetCopyTopic = '%env(MESSENGER_ASSET_COPY_TOPIC)%';
+ $assetCopyTopicDsn = "%env(MESSENGER_TRANSPORT_DSN)%/{$assetCopyTopic}";
$container->prependExtensionConfig('framework', [
'messenger' => [
@@ -362,6 +365,30 @@ public function prepend(ContainerBuilder $container): void
],
],
],
+ $assetCopyTopic => [
+ 'dsn' => $assetCopyTopicDsn,
+ 'options' => [
+ 'topic' => [
+ 'name' => $assetCopyTopic,
+ 'options' => [
+ 'labels' => [
+ 'application' => $applicationName,
+ 'name' => $assetCopyTopic,
+ 'topic' => $assetCopyTopic,
+ ],
+ ],
+ ],
+ 'subscription' => [
+ 'name' => $assetCopyTopic,
+ 'options' => [
+ 'labels' => [
+ 'application' => $applicationName,
+ 'name' => $assetCopyTopic,
+ ],
+ ],
+ ],
+ ],
+ ],
],
'routing' => [
VideoFileChangeStateMessage::class => $videoFileChangeStateTopic,
@@ -374,6 +401,7 @@ public function prepend(ContainerBuilder $container): void
DistributionRemoteProcessingCheckMessage::class => $distributionRemoteProcessedCheckTopic,
JwVideoThumbnailPosterMessage::class => $distributionRemoteProcessedCheckTopic,
AssetRefreshPropertiesMessage::class => $assetPropertyRefreshTopic,
+ CopyAssetFileMessage::class => $assetCopyTopic,
],
],
]);
diff --git a/src/Domain/Asset/AssetCopyBuilder.php b/src/Domain/Asset/AssetCopyBuilder.php
new file mode 100644
index 00000000..8597d001
--- /dev/null
+++ b/src/Domain/Asset/AssetCopyBuilder.php
@@ -0,0 +1,68 @@
+__copy();
+ $assetCopy->getAttributes()->setStatus(AssetStatus::Draft);
+ $this->trackCreation($assetCopy);
+ $assetCopy->setLicence($assetLicence);
+ $assetCopy->setExtSystem($assetLicence->getExtSystem());
+ $this->entityManager->persist($assetCopy);
+ $this->assetMetadataManager->create($assetCopy->getMetadata(), false);
+ $this->copySlots($asset, $assetCopy);
+
+ foreach ($assetCopy->getSlots() as $assetSlot) {
+ if ($assetSlot->getFlags()->isMain()) {
+ $assetCopy->setMainFile($assetSlot->getAssetFile());
+
+ break;
+ }
+ }
+
+ $this->flush($flush);
+
+ return $assetCopy;
+ }
+
+ private function copySlots(Asset $asset, Asset $assetCopy): void
+ {
+ foreach ($asset->getSlots() as $assetSlot) {
+ $slotCopy = $this->copySlotToAsset($assetSlot, $assetCopy);
+ $assetCopy->addSlot($slotCopy);
+ $slotCopy->setAsset($assetCopy);
+ }
+ }
+
+ private function copySlotToAsset(AssetSlot $assetSlot, Asset $assetCopy): AssetSlot
+ {
+ $slotCopy = $assetSlot->__copy();
+ $blankAssetFile = $this->assetFileFactory->createForAsset($assetCopy);
+ $blankAssetFile->setAsset($assetCopy);
+ $blankAssetFile->addSlot($slotCopy);
+ $this->assetSlotManager->create($slotCopy, false);
+
+ return $slotCopy;
+ }
+}
diff --git a/src/Domain/AssetFile/AbstractAssetFileFactory.php b/src/Domain/AssetFile/AbstractAssetFileFactory.php
index 0c860000..80271eac 100644
--- a/src/Domain/AssetFile/AbstractAssetFileFactory.php
+++ b/src/Domain/AssetFile/AbstractAssetFileFactory.php
@@ -12,6 +12,7 @@
use AnzuSystems\CoreDamBundle\Domain\AssetFileMetadata\AssetFileMetadataManager;
use AnzuSystems\CoreDamBundle\Domain\Configuration\ExtSystemConfigurationProvider;
use AnzuSystems\CoreDamBundle\Domain\Image\ImageStatusFacade;
+use AnzuSystems\CoreDamBundle\Entity\Asset;
use AnzuSystems\CoreDamBundle\Entity\AssetFile;
use AnzuSystems\CoreDamBundle\Entity\AssetFileMetadata;
use AnzuSystems\CoreDamBundle\Entity\AssetLicence;
@@ -104,6 +105,19 @@ public function createFromFile(AdapterFile $file, AssetLicence $assetLicence, ?s
return $assetFile;
}
+ public function createForAsset(Asset $asset): AssetFile
+ {
+ /** @var T $assetFile */
+ $assetFile = match ($asset->getAssetType()) {
+ AssetType::Image => $this->createBlankImage($asset->getLicence()),
+ AssetType::Video => $this->createBlankVideo($asset->getLicence()),
+ AssetType::Audio => $this->createBlankAudio($asset->getLicence()),
+ AssetType::Document => $this->createBlankDocument($asset->getLicence()),
+ };
+
+ return $this->assetFileManager->create($assetFile, false);
+ }
+
/**
* @return T
*/
diff --git a/src/Domain/AssetFile/AssetFileCopyBuilder.php b/src/Domain/AssetFile/AssetFileCopyBuilder.php
new file mode 100644
index 00000000..f7e5064c
--- /dev/null
+++ b/src/Domain/AssetFile/AssetFileCopyBuilder.php
@@ -0,0 +1,43 @@
+setAssetAttributes(clone $assetFile->getAssetAttributes());
+ $this->assetFileStorageOperator->copyToAssetFile($assetFile, $targetAssetFile);
+ if ($assetFile instanceof ImageFile && $targetAssetFile instanceof ImageFile) {
+ $this->imageFileCopyBuilder->copy($assetFile, $targetAssetFile);
+
+ return;
+ }
+
+ throw new RuntimeException(
+ sprintf(
+ 'Unsupported copy AssetFile combination. Copy from (%s) to (%s)',
+ $assetFile::class,
+ $targetAssetFile::class
+ )
+ );
+ }
+}
diff --git a/src/Domain/AssetFile/AssetFileStatusManager.php b/src/Domain/AssetFile/AssetFileStatusManager.php
index d93c7fc0..cb4d5de0 100644
--- a/src/Domain/AssetFile/AssetFileStatusManager.php
+++ b/src/Domain/AssetFile/AssetFileStatusManager.php
@@ -78,7 +78,7 @@ public function toProcessed(AssetFile $assetFile): AssetFile
/**
* @throws SerializerException
*/
- public function toFailed(AssetFile $assetFile, AssetFileFailedType $failedType, Throwable $throwable): AssetFile
+ public function toFailed(AssetFile $assetFile, AssetFileFailedType $failedType, ?Throwable $throwable = null): AssetFile
{
$this->changeTransition($assetFile, AssetFileProcessStatus::Failed, $failedType);
@@ -88,7 +88,7 @@ public function toFailed(AssetFile $assetFile, AssetFileFailedType $failedType,
'Asset file (%s) process failed reason (%s). (%s',
(string) $assetFile->getId(),
$failedType->toString(),
- $throwable->getMessage()
+ $throwable?->getMessage()
),
exception: $throwable
);
diff --git a/src/Domain/AssetFile/FileProcessor/AssetFileStorageOperator.php b/src/Domain/AssetFile/FileProcessor/AssetFileStorageOperator.php
index e9d47005..83b32e54 100644
--- a/src/Domain/AssetFile/FileProcessor/AssetFileStorageOperator.php
+++ b/src/Domain/AssetFile/FileProcessor/AssetFileStorageOperator.php
@@ -42,4 +42,26 @@ public function save(AssetFile $assetFile, AdapterFile $file): AssetFile
return $assetFile;
}
+
+ /**
+ * @throws FilesystemException
+ */
+ public function copyToAssetFile(AssetFile $sourceAssetFile, AssetFile $targetAssetFile): void
+ {
+ $path = $this->nameGenerator->generatePath(
+ $this->fileHelper->guessExtension($sourceAssetFile->getAssetAttributes()->getMimeType()),
+ true
+ );
+
+ $sourceFileSystem = $this->fileSystemProvider->getFilesystemByStorable($sourceAssetFile);
+ $targetFileSystem = $this->fileSystemProvider->getFilesystemByStorable($targetAssetFile);
+
+ $targetFileSystem->writeStream(
+ $path->getRelativePath(),
+ $sourceFileSystem->readStream($sourceAssetFile->getFilePath())
+ );
+
+ $targetAssetFile->getAssetAttributes()
+ ->setFilePath($path->getRelativePath());
+ }
}
diff --git a/src/Domain/Image/ImageCopyFacade.php b/src/Domain/Image/ImageCopyFacade.php
new file mode 100644
index 00000000..f58c4cf4
--- /dev/null
+++ b/src/Domain/Image/ImageCopyFacade.php
@@ -0,0 +1,204 @@
+ $collection
+ * @return Collection
+ *
+ * @throws Throwable
+ */
+ public function prepareCopyList(Collection $collection): Collection
+ {
+ $this->validateMaxBulkCount($collection);
+ $this->validator->validate($collection);
+
+ /** @var AssetFileCopyResultDto[] $res */
+ $res = [];
+
+ try {
+ $this->entityManager->beginTransaction();
+
+ foreach ($collection as $imageCopyDto) {
+ $this->accessDenier->denyUnlessGranted(DamPermissions::DAM_ASSET_READ, $imageCopyDto->getAsset()->getLicence());
+ $this->accessDenier->denyUnlessGranted(DamPermissions::DAM_ASSET_CREATE, $imageCopyDto->getTargetAssetLicence());
+
+ $resDto = $this->prepareCopy($imageCopyDto);
+ $res[] = $resDto;
+ }
+
+ $this->entityManager->commit();
+ } catch (Throwable $exception) {
+ if ($this->entityManager->getConnection()->isTransactionActive()) {
+ $this->entityManager->rollback();
+ }
+
+ throw $exception;
+ }
+
+ foreach ($res as $imageCopyResultDto) {
+ if ($imageCopyResultDto->getResult()->is(AssetFileCopyResult::Copying) && $imageCopyResultDto->getTargetAsset()) {
+ $this->messageBus->dispatch(new CopyAssetFileMessage(
+ $imageCopyResultDto->getAsset(),
+ $imageCopyResultDto->getTargetAsset()
+ ));
+ }
+ }
+
+ return new ArrayCollection($res);
+ }
+
+ /**
+ * @throws Throwable
+ */
+ public function copyAssetFiles(Asset $asset, Asset $copyAsset): void
+ {
+ try {
+ $this->entityManager->beginTransaction();
+ $this->copyAssetSlots($asset, $copyAsset);
+ $this->assetManager->updateExisting(asset: $copyAsset, trackModification: false);
+ $this->indexManager->index($copyAsset);
+ $this->entityManager->commit();
+ } catch (Throwable $exception) {
+ if ($this->entityManager->getConnection()->isTransactionActive()) {
+ $this->entityManager->rollback();
+ }
+
+ throw $exception;
+ }
+
+ foreach ($copyAsset->getSlots() as $slot) {
+ $this->assetFileEventDispatcher->dispatchAssetFileChanged($slot->getAssetFile());
+ }
+ }
+
+ private function prepareCopy(ImageCopyDto $copyDto): AssetFileCopyResultDto
+ {
+ /** @var array $foundAssets */
+ $foundAssets = [];
+ foreach ($copyDto->getAsset()->getSlots() as $slot) {
+ $foundAssetFile = $this->imageFileRepository->findProcessedByChecksumAndLicence(
+ checksum: $slot->getAssetFile()->getAssetAttributes()->getChecksum(),
+ licence: $copyDto->getTargetAssetLicence()
+ );
+
+ if (null === $foundAssetFile) {
+ continue;
+ }
+
+ $foundAssets[(string) $foundAssetFile->getAsset()->getId()] = $foundAssetFile->getAsset();
+ }
+
+ $firstFoundAsset = $foundAssets[(string) array_key_first($foundAssets)] ?? null;
+
+ if (null === $firstFoundAsset) {
+ $assetCopy = $this->assetCopyBuilder->buildDraftAssetCopy($copyDto->getAsset(), $copyDto->getTargetAssetLicence());
+
+ return AssetFileCopyResultDto::create(
+ asset: $copyDto->getAsset(),
+ targetAssetLicence: $copyDto->getTargetAssetLicence(),
+ result: AssetFileCopyResult::Copying,
+ targetMainFile: $assetCopy->getMainFile(),
+ targetAsset: $assetCopy,
+ );
+ }
+
+ if (count($foundAssets) > 1 || false === $firstFoundAsset->hasSameFilesIdentityString($copyDto->getAsset())) {
+ return AssetFileCopyResultDto::create(
+ asset: $copyDto->getAsset(),
+ targetAssetLicence: $copyDto->getTargetAssetLicence(),
+ result: AssetFileCopyResult::NotAllowed,
+ assetConflicts: array_values($foundAssets)
+ );
+ }
+
+ return AssetFileCopyResultDto::create(
+ asset: $copyDto->getAsset(),
+ targetAssetLicence: $copyDto->getTargetAssetLicence(),
+ result: AssetFileCopyResult::Exists,
+ targetMainFile: $firstFoundAsset->getMainFile(),
+ targetAsset: $firstFoundAsset,
+ );
+ }
+
+ private function copyAssetSlots(Asset $asset, Asset $copyAsset): void
+ {
+ foreach ($copyAsset->getSlots() as $targetSlot) {
+ $assetSlot = $asset->getSlots()->findFirst(
+ fn (int $index, AssetSlot $assetSlot) => $assetSlot->getName() === $targetSlot->getName()
+ );
+
+ if ($assetSlot instanceof AssetSlot) {
+ $this->assetFileCopyBuilder->copy($assetSlot->getAssetFile(), $targetSlot->getAssetFile());
+
+ continue;
+ }
+
+ $this->assetFileStatusManager->toFailed(
+ $targetSlot->getAssetFile(),
+ AssetFileFailedType::Unknown
+ );
+ }
+ }
+
+ /**
+ * @param Collection $dtoList
+ */
+ private function validateMaxBulkCount(Collection $dtoList): void
+ {
+ if ($dtoList->count() > self::BULK_COPY_SIZE) {
+ throw new ForbiddenOperationException(ForbiddenOperationException::DETAIL_BULK_SIZE_EXCEEDED);
+ }
+ }
+}
diff --git a/src/Domain/Image/ImageFileCopyBuilder.php b/src/Domain/Image/ImageFileCopyBuilder.php
new file mode 100644
index 00000000..cb107a56
--- /dev/null
+++ b/src/Domain/Image/ImageFileCopyBuilder.php
@@ -0,0 +1,46 @@
+setImageAttributes(clone $imageFile->getImageAttributes());
+ $this->copyRegionsOfInterest($imageFile, $targetImageFile);
+ $this->copyResizes($imageFile, $targetImageFile);
+ }
+
+ private function copyResizes(ImageFile $imageFile, ImageFile $targetImageFile): void
+ {
+ foreach ($imageFile->getResizes() as $resize) {
+ $this->imageFileOptimalResizeCopyBuilder->copyResizeToImage($resize, $targetImageFile);
+ }
+ }
+
+ private function copyRegionsOfInterest(ImageFile $imageFile, ImageFile $targetImageFile): void
+ {
+ foreach ($imageFile->getRegionsOfInterest() as $regionOfInterest) {
+ $regionOfInterestCopy = $regionOfInterest->__copy();
+ $regionOfInterestCopy->setImage($targetImageFile);
+ $targetImageFile->getRegionsOfInterest()->add($regionOfInterestCopy);
+ $this->regionOfInterestManager->create($regionOfInterestCopy, false);
+ }
+ }
+}
diff --git a/src/Domain/ImageFileOptimalResize/ImageFileOptimalResizeCopyBuilder.php b/src/Domain/ImageFileOptimalResize/ImageFileOptimalResizeCopyBuilder.php
new file mode 100644
index 00000000..1e661b96
--- /dev/null
+++ b/src/Domain/ImageFileOptimalResize/ImageFileOptimalResizeCopyBuilder.php
@@ -0,0 +1,41 @@
+optimalResizeFactory->createOptimalCropPath(
+ $targetImageFile,
+ $resize->getRequestedSize(),
+ $targetImageFile->getImageAttributes()->getRotation()
+ );
+
+ $resizeCopy = $resize->__copy();
+ $resizeCopy->setFilePath($targetPath);
+ $resizeCopy->setImage($targetImageFile);
+ $targetImageFile->getResizes()->add($resizeCopy);
+
+ $this->fileSystemProvider->getFilesystemByStorable($resizeCopy)->writeStream(
+ location: $targetPath,
+ contents: $this->fileSystemProvider->getFilesystemByStorable($resize)->readStream($resize->getFilePath())
+ );
+
+ $this->optimalResizeManager->create($resizeCopy, false);
+ }
+}
diff --git a/src/Elasticsearch/QueryFactory/AssetQueryFactory.php b/src/Elasticsearch/QueryFactory/AssetQueryFactory.php
index 5f6a2f69..1ffaa9ce 100644
--- a/src/Elasticsearch/QueryFactory/AssetQueryFactory.php
+++ b/src/Elasticsearch/QueryFactory/AssetQueryFactory.php
@@ -12,7 +12,6 @@
use AnzuSystems\CoreDamBundle\Entity\AssetLicence;
use AnzuSystems\CoreDamBundle\Entity\CustomFormElement;
use AnzuSystems\CoreDamBundle\Entity\ExtSystem;
-use AnzuSystems\CoreDamBundle\Helper\UuidHelper;
final class AssetQueryFactory extends AbstractQueryFactory
{
@@ -41,7 +40,7 @@ protected function getMust(SearchDtoInterface $searchDto, ExtSystem $extSystem):
$customDataFields = array_unique($customDataFields);
$customDataFields = array_merge($customDataFields, ['title']);
- if (UuidHelper::isUuid($searchDto->getText())) {
+ if (is_string($searchDto->getIdInText())) {
return parent::getMust($searchDto, $extSystem);
}
@@ -70,8 +69,8 @@ protected function getFilter(SearchDtoInterface $searchDto): array
$this->applyLicenceCollectionFilter($filter, $searchDto);
}
- if (UuidHelper::isUuid($searchDto->getText())) {
- $filter[] = $this->getAssetIdAndMainFileIdFilter([$searchDto->getText()]);
+ if (is_string($searchDto->getIdInText())) {
+ $filter[] = $this->getAssetIdAndMainFileIdFilter([$searchDto->getIdInText()]);
// other filters should not be applied
return $filter;
diff --git a/src/Elasticsearch/QueryFactory/AuthorQueryFactory.php b/src/Elasticsearch/QueryFactory/AuthorQueryFactory.php
index cbb906f1..ddc4a3cd 100644
--- a/src/Elasticsearch/QueryFactory/AuthorQueryFactory.php
+++ b/src/Elasticsearch/QueryFactory/AuthorQueryFactory.php
@@ -40,7 +40,6 @@ protected function getMust(SearchDtoInterface $searchDto, ExtSystem $extSystem):
/**
* @param AuthorAdmSearchDto $searchDto
-
* @psalm-suppress PossiblyNullReference
*/
protected function getFilter(SearchDtoInterface $searchDto): array
diff --git a/src/Elasticsearch/QueryFactory/DistributionQueryFactory.php b/src/Elasticsearch/QueryFactory/DistributionQueryFactory.php
index d806c427..beffd280 100644
--- a/src/Elasticsearch/QueryFactory/DistributionQueryFactory.php
+++ b/src/Elasticsearch/QueryFactory/DistributionQueryFactory.php
@@ -18,7 +18,6 @@ public function getSupportedSearchDtoClasses(): array
/**
* @param DistributionAdmSearchDto $searchDto
-
* @psalm-suppress PossiblyNullReference
*/
protected function getFilter(SearchDtoInterface $searchDto): array
diff --git a/src/Elasticsearch/QueryFactory/KeywordQueryFactory.php b/src/Elasticsearch/QueryFactory/KeywordQueryFactory.php
index 0f7a04a1..2c314e5b 100644
--- a/src/Elasticsearch/QueryFactory/KeywordQueryFactory.php
+++ b/src/Elasticsearch/QueryFactory/KeywordQueryFactory.php
@@ -40,7 +40,6 @@ protected function getMust(SearchDtoInterface $searchDto, ExtSystem $extSystem):
/**
* @param KeywordAdmSearchDto $searchDto
-
* @psalm-suppress PossiblyNullReference
*/
protected function getFilter(SearchDtoInterface $searchDto): array
diff --git a/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php b/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php
index 7cbf2e98..9567631a 100644
--- a/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php
+++ b/src/Elasticsearch/SearchDto/AssetAdmSearchDto.php
@@ -7,6 +7,8 @@
use AnzuSystems\CommonBundle\Exception\ValidationException;
use AnzuSystems\CoreDamBundle\Entity\Asset;
use AnzuSystems\CoreDamBundle\Entity\AssetLicence;
+use AnzuSystems\CoreDamBundle\Helper\UrlHelper;
+use AnzuSystems\CoreDamBundle\Helper\UuidHelper;
use AnzuSystems\CoreDamBundle\Model\Enum\AssetStatus;
use AnzuSystems\CoreDamBundle\Model\Enum\AssetType;
use AnzuSystems\CoreDamBundle\Model\Enum\ImageOrientation;
@@ -179,6 +181,8 @@ class AssetAdmSearchDto extends AbstractSearchDto
#[Serialize]
private ?DateTimeImmutable $createdAtUntil = null;
+ private ?string $idInText = null;
+ private bool $resolvedIdInText = false;
public function getIndexName(): string
{
@@ -698,4 +702,23 @@ public function setAssetAndMainFileIds(array $assetAndMainFileIds): self
return $this;
}
+
+ public function getIdInText(): ?string
+ {
+ if (false === $this->resolvedIdInText) {
+ $this->idInText = $this->resolveIdFromText();
+ $this->resolvedIdInText = true;
+ }
+
+ return $this->idInText;
+ }
+
+ private function resolveIdFromText(): ?string
+ {
+ if (UuidHelper::isUuid($this->getText())) {
+ return $this->getText();
+ }
+
+ return UrlHelper::getImageIdFromUrl($this->getText());
+ }
}
diff --git a/src/Entity/Asset.php b/src/Entity/Asset.php
index 0eb8f22c..f4be684e 100644
--- a/src/Entity/Asset.php
+++ b/src/Entity/Asset.php
@@ -4,6 +4,7 @@
namespace AnzuSystems\CoreDamBundle\Entity;
+use AnzuSystems\Contracts\Entity\Interfaces\CopyableInterface;
use AnzuSystems\Contracts\Entity\Interfaces\TimeTrackingInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UserTrackingInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UuidIdentifiableInterface;
@@ -34,7 +35,7 @@
* @psalm-method DamUser getModifiedBy()
*/
#[ORM\Entity(repositoryClass: AssetRepository::class)]
-#[ORM\Index(fields: ['attributes.status', 'createdAt', 'assetFlags.autoDeleteUnprocessed'], name: 'IDX_status_created_auto_delete')]
+#[ORM\Index(name: 'IDX_status_created_auto_delete', fields: ['attributes.status', 'createdAt', 'assetFlags.autoDeleteUnprocessed'])]
class Asset implements
TimeTrackingInterface,
UuidIdentifiableInterface,
@@ -42,7 +43,8 @@ class Asset implements
ExtSystemIndexableInterface,
NotifiableInterface,
AssetCustomFormProvidableInterface,
- AssetLicenceInterface
+ AssetLicenceInterface,
+ CopyableInterface
{
use TimeTrackingTrait;
use UuidIdentityTrait;
@@ -60,10 +62,10 @@ class Asset implements
#[ORM\ManyToMany(targetEntity: Keyword::class, fetch: App::DOCTRINE_EXTRA_LAZY, indexBy: 'id')]
private Collection $keywords;
- #[ORM\OneToMany(mappedBy: 'asset', targetEntity: PodcastEpisode::class, fetch: App::DOCTRINE_EXTRA_LAZY)]
+ #[ORM\OneToMany(targetEntity: PodcastEpisode::class, mappedBy: 'asset', fetch: App::DOCTRINE_EXTRA_LAZY)]
private Collection $episodes;
- #[ORM\OneToMany(mappedBy: 'asset', targetEntity: VideoShowEpisode::class, fetch: App::DOCTRINE_EXTRA_LAZY)]
+ #[ORM\OneToMany(targetEntity: VideoShowEpisode::class, mappedBy: 'asset', fetch: App::DOCTRINE_EXTRA_LAZY)]
private Collection $videoEpisodes;
#[ORM\Embedded(class: AssetTexts::class)]
@@ -116,6 +118,29 @@ public function __construct()
$this->setExtSystem(null);
}
+ public function __copy(): self
+ {
+ return (new self())
+ ->setAttributes(clone $this->getAttributes())
+ ->setTexts(clone $this->getTexts())
+ ->setAssetFlags(clone $this->getAssetFlags())
+ ->setAssetFileProperties(clone $this->getAssetFileProperties())
+ ->setMetadata($this->getMetadata()->__copy())
+// ->setSlots(clone $this->getSlots())
+ ->setAuthors($this->getAuthors())
+ ->setKeywords($this->getKeywords())
+ ->setDistributionCategory($this->getDistributionCategory())
+ ->setMainFile(null)
+
+ ->setExtSystem($this->getExtSystem())
+ ;
+ }
+
+ public function __clone()
+ {
+
+ }
+
public function getMainFile(): ?AssetFile
{
return $this->mainFile;
@@ -401,4 +426,21 @@ public static function getIndexName(): string
{
return self::getResourceName();
}
+
+ public function hasSameFilesIdentityString(self $asset): bool
+ {
+ return $asset->getFilesIdentityString() === $this->getFilesIdentityString();
+ }
+
+ public function getFilesIdentityString(): string
+ {
+ $identityParts = [];
+ foreach ($this->getSlots() as $slot) {
+ $identityParts[$slot->getName()] =
+ $slot->getName() . ':' . $slot->getAssetFile()->getAssetAttributes()->getChecksum();
+ }
+ ksort($identityParts);
+
+ return implode('_', $identityParts);
+ }
}
diff --git a/src/Entity/AssetFile.php b/src/Entity/AssetFile.php
index 45b9ceb7..b42efd7d 100644
--- a/src/Entity/AssetFile.php
+++ b/src/Entity/AssetFile.php
@@ -4,6 +4,7 @@
namespace AnzuSystems\CoreDamBundle\Entity;
+use AnzuSystems\Contracts\Entity\Interfaces\CopyableInterface;
use AnzuSystems\Contracts\Entity\Interfaces\TimeTrackingInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UserTrackingInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UuidIdentifiableInterface;
@@ -28,9 +29,9 @@
* @psalm-method DamUser getModifiedBy()
*/
#[ORM\Entity(repositoryClass: AssetFileRepository::class)]
-#[ORM\Index(fields: ['licence', 'assetAttributes.originExternalProvider'], name: 'IDX_licence_attributes_external_provider')]
-#[ORM\Index(fields: ['assetAttributes.status'], name: 'IDX_attributes_status')]
-#[ORM\Index(fields: ['licence', 'assetAttributes.status', 'assetAttributes.checksum'], name: 'IDX_licence_attributes_status_checksum')]
+#[ORM\Index(name: 'IDX_licence_attributes_external_provider', fields: ['licence', 'assetAttributes.originExternalProvider'])]
+#[ORM\Index(name: 'IDX_attributes_status', fields: ['assetAttributes.status'])]
+#[ORM\Index(name: 'IDX_licence_attributes_status_checksum', fields: ['licence', 'assetAttributes.status', 'assetAttributes.checksum'])]
#[ORM\InheritanceType(value: 'JOINED')]
abstract class AssetFile implements
TimeTrackingInterface,
@@ -39,21 +40,22 @@ abstract class AssetFile implements
UserTrackingInterface,
FileSystemStorableInterface,
NotifiableInterface,
- AssetLicenceInterface
+ AssetLicenceInterface,
+ CopyableInterface
{
use TimeTrackingTrait;
use UuidIdentityTrait;
use UserTrackingTrait;
use NotifyToTrait;
- #[ORM\OneToMany(mappedBy: 'assetFile', targetEntity: Chunk::class, fetch: App::DOCTRINE_EXTRA_LAZY)]
+ #[ORM\OneToMany(targetEntity: Chunk::class, mappedBy: 'assetFile', fetch: App::DOCTRINE_EXTRA_LAZY)]
#[ORM\OrderBy(value: ['offset' => App::ORDER_ASC])]
protected Collection $chunks;
#[ORM\OneToOne(targetEntity: AssetFileMetadata::class)]
protected AssetFileMetadata $metadata;
- #[ORM\OneToMany(mappedBy: 'targetAssetFile', targetEntity: AssetFileRoute::class, fetch: App::DOCTRINE_EXTRA_LAZY)]
+ #[ORM\OneToMany(targetEntity: AssetFileRoute::class, mappedBy: 'targetAssetFile', fetch: App::DOCTRINE_EXTRA_LAZY)]
protected Collection $routes;
#[ORM\OneToOne(targetEntity: AssetFileRoute::class)]
@@ -187,4 +189,20 @@ public function getExtSystem(): ExtSystem
{
return $this->getLicence()->getExtSystem();
}
+
+ protected function copyBase(self $assetFile): static
+ {
+ $this->setAssetAttributes(new AssetFileAttributes());
+ $this->setCreatedAt(App::getAppDate());
+ $this->setModifiedAt(App::getAppDate());
+ $this->setChunks(new ArrayCollection());
+ $this->setFlags(new AssetFileFlags());
+ $this->setRoutes(new ArrayCollection());
+ $this->setMainRoute(null);
+
+ return $assetFile
+ ->setAssetAttributes(clone $this->getAssetAttributes())
+ ->setFlags(clone $this->getFlags())
+ ;
+ }
}
diff --git a/src/Entity/AssetMetadata.php b/src/Entity/AssetMetadata.php
index 54af4243..37b0b18f 100644
--- a/src/Entity/AssetMetadata.php
+++ b/src/Entity/AssetMetadata.php
@@ -4,6 +4,7 @@
namespace AnzuSystems\CoreDamBundle\Entity;
+use AnzuSystems\Contracts\Entity\Interfaces\CopyableInterface;
use AnzuSystems\Contracts\Entity\Interfaces\TimeTrackingInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UserTrackingInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UuidIdentifiableInterface;
@@ -16,7 +17,7 @@
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: CustomFormRepository::class)]
-class AssetMetadata implements TimeTrackingInterface, UuidIdentifiableInterface, UserTrackingInterface
+class AssetMetadata implements TimeTrackingInterface, UuidIdentifiableInterface, UserTrackingInterface, CopyableInterface
{
use TimeTrackingTrait;
use UuidIdentityTrait;
@@ -41,6 +42,15 @@ public function __construct()
$this->setAuthorSuggestions([]);
}
+ public function __copy(): self
+ {
+ return (new self())
+ ->setCustomData($this->getCustomData())
+ ->setKeywordSuggestions($this->getKeywordSuggestions())
+ ->setAuthorSuggestions($this->getAuthorSuggestions())
+ ;
+ }
+
public function getCustomData(): array
{
return $this->customData;
diff --git a/src/Entity/AssetSlot.php b/src/Entity/AssetSlot.php
index 34ee819a..32c385ee 100644
--- a/src/Entity/AssetSlot.php
+++ b/src/Entity/AssetSlot.php
@@ -4,6 +4,7 @@
namespace AnzuSystems\CoreDamBundle\Entity;
+use AnzuSystems\Contracts\Entity\Interfaces\CopyableInterface;
use AnzuSystems\Contracts\Entity\Interfaces\TimeTrackingInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UserTrackingInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UuidIdentifiableInterface;
@@ -17,10 +18,10 @@
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: AssetSlotRepository::class)]
-#[ORM\Index(fields: ['name'], name: 'IDX_name')]
-#[ORM\Index(fields: ['flags.default'], name: 'IDX_default')]
+#[ORM\Index(name: 'IDX_name', fields: ['name'])]
+#[ORM\Index(name: 'IDX_default', fields: ['flags.default'])]
#[ORM\UniqueConstraint(name: 'UNIQ_asset_file_asset_name', fields: ['asset', 'name'])]
-class AssetSlot implements UuidIdentifiableInterface, TimeTrackingInterface, UserTrackingInterface
+class AssetSlot implements UuidIdentifiableInterface, TimeTrackingInterface, UserTrackingInterface, CopyableInterface
{
use UuidIdentityTrait;
use UserTrackingTrait;
@@ -57,6 +58,14 @@ public function __construct()
$this->setDocument(null);
}
+ public function __copy(): self
+ {
+ return (new self())
+ ->setName($this->getName())
+ ->setFlags(clone $this->getFlags())
+ ;
+ }
+
public function getFlags(): AssetSlotFlags
{
return $this->flags;
diff --git a/src/Entity/AudioFile.php b/src/Entity/AudioFile.php
index a67da1f1..8ab7b9c6 100644
--- a/src/Entity/AudioFile.php
+++ b/src/Entity/AudioFile.php
@@ -22,7 +22,7 @@ class AudioFile extends AssetFile
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private Asset $asset;
- #[ORM\OneToMany(mappedBy: 'audio', targetEntity: AssetSlot::class, fetch: App::DOCTRINE_EXTRA_LAZY)]
+ #[ORM\OneToMany(targetEntity: AssetSlot::class, mappedBy: 'audio', fetch: App::DOCTRINE_EXTRA_LAZY)]
private Collection $slots;
public function __construct()
@@ -32,6 +32,15 @@ public function __construct()
parent::__construct();
}
+ public function __copy(): self
+ {
+ $assetFile = (new self())
+ ->setAttributes(clone $this->getAttributes())
+ ;
+
+ return parent::copyBase($assetFile);
+ }
+
public function getAttributes(): AudioAttributes
{
return $this->attributes;
diff --git a/src/Entity/DocumentFile.php b/src/Entity/DocumentFile.php
index 4275e06a..4e36d7d6 100644
--- a/src/Entity/DocumentFile.php
+++ b/src/Entity/DocumentFile.php
@@ -22,7 +22,7 @@ class DocumentFile extends AssetFile
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private Asset $asset;
- #[ORM\OneToMany(mappedBy: 'document', targetEntity: AssetSlot::class, fetch: App::DOCTRINE_EXTRA_LAZY)]
+ #[ORM\OneToMany(targetEntity: AssetSlot::class, mappedBy: 'document', fetch: App::DOCTRINE_EXTRA_LAZY)]
private Collection $slots;
public function __construct()
@@ -32,6 +32,15 @@ public function __construct()
parent::__construct();
}
+ public function __copy(): self
+ {
+ $assetFile = (new self())
+ ->setAttributes(clone $this->getAttributes())
+ ;
+
+ return parent::copyBase($assetFile);
+ }
+
public function getAttributes(): DocumentAttributes
{
return $this->attributes;
diff --git a/src/Entity/ImageFile.php b/src/Entity/ImageFile.php
index 1598f1ec..77e8ab6e 100644
--- a/src/Entity/ImageFile.php
+++ b/src/Entity/ImageFile.php
@@ -18,18 +18,18 @@ class ImageFile extends AssetFile
#[ORM\Embedded(class: ImageAttributes::class)]
private ImageAttributes $imageAttributes;
- #[ORM\OneToMany(mappedBy: 'image', targetEntity: ImageFileOptimalResize::class)]
+ #[ORM\OneToMany(targetEntity: ImageFileOptimalResize::class, mappedBy: 'image')]
#[ORM\OrderBy(value: ['requestedSize' => App::ORDER_ASC])]
private Collection $resizes;
- #[ORM\OneToMany(mappedBy: 'image', targetEntity: RegionOfInterest::class)]
+ #[ORM\OneToMany(targetEntity: RegionOfInterest::class, mappedBy: 'image')]
private Collection $regionsOfInterest;
#[ORM\ManyToOne(targetEntity: Asset::class)]
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private Asset $asset;
- #[ORM\OneToMany(mappedBy: 'image', targetEntity: AssetSlot::class, fetch: App::DOCTRINE_EXTRA_LAZY)]
+ #[ORM\OneToMany(targetEntity: AssetSlot::class, mappedBy: 'image', fetch: App::DOCTRINE_EXTRA_LAZY)]
private Collection $slots;
public function __construct()
@@ -42,6 +42,20 @@ public function __construct()
parent::__construct();
}
+ public function __copy(): self
+ {
+ $regionsOfInterest = $this->getRegionsOfInterest()->map(
+ static fn (RegionOfInterest $regionOfInterest): RegionOfInterest => $regionOfInterest->__copy()
+ );
+
+ $assetFile = (new self())
+ ->setImageAttributes(clone $this->getImageAttributes())
+ ->setRegionsOfInterest($regionsOfInterest)
+ ;
+
+ return parent::copyBase($assetFile);
+ }
+
/**
* @return Collection
*/
diff --git a/src/Entity/ImageFileOptimalResize.php b/src/Entity/ImageFileOptimalResize.php
index 81a8940f..78c7806d 100644
--- a/src/Entity/ImageFileOptimalResize.php
+++ b/src/Entity/ImageFileOptimalResize.php
@@ -4,6 +4,7 @@
namespace AnzuSystems\CoreDamBundle\Entity;
+use AnzuSystems\Contracts\Entity\Interfaces\CopyableInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UuidIdentifiableInterface;
use AnzuSystems\CoreDamBundle\Entity\Interfaces\FileSystemStorableInterface;
use AnzuSystems\CoreDamBundle\Entity\Traits\UuidIdentityTrait;
@@ -13,7 +14,7 @@
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: ImageFileOptimalResizeRepository::class)]
-class ImageFileOptimalResize implements UuidIdentifiableInterface, FileSystemStorableInterface
+class ImageFileOptimalResize implements UuidIdentifiableInterface, FileSystemStorableInterface, CopyableInterface
{
use UuidIdentityTrait;
@@ -44,6 +45,16 @@ public function __construct()
$this->setOriginal(false);
}
+ public function __copy(): self
+ {
+ return (new self())
+ ->setWidth($this->getWidth())
+ ->setHeight($this->getHeight())
+ ->setRequestedSize($this->getRequestedSize())
+ ->setOriginal($this->isOriginal())
+ ;
+ }
+
public function getRequestedSize(): int
{
return $this->requestedSize;
diff --git a/src/Entity/RegionOfInterest.php b/src/Entity/RegionOfInterest.php
index 9c077ccc..954c4754 100644
--- a/src/Entity/RegionOfInterest.php
+++ b/src/Entity/RegionOfInterest.php
@@ -4,6 +4,7 @@
namespace AnzuSystems\CoreDamBundle\Entity;
+use AnzuSystems\Contracts\Entity\Interfaces\CopyableInterface;
use AnzuSystems\Contracts\Entity\Interfaces\TimeTrackingInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UserTrackingInterface;
use AnzuSystems\Contracts\Entity\Interfaces\UuidIdentifiableInterface;
@@ -18,8 +19,8 @@
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: RegionOfInterestRepository::class)]
-#[ORM\Index(fields: ['position'], name: 'IDX_position')]
-class RegionOfInterest implements UuidIdentifiableInterface, UserTrackingInterface, TimeTrackingInterface, PositionableInterface, AssetLicenceInterface
+#[ORM\Index(name: 'IDX_position', fields: ['position'])]
+class RegionOfInterest implements UuidIdentifiableInterface, UserTrackingInterface, TimeTrackingInterface, PositionableInterface, AssetLicenceInterface, CopyableInterface
{
use UuidIdentityTrait;
use TimeTrackingTrait;
@@ -55,6 +56,17 @@ public function __construct()
$this->setTitle('');
}
+ public function __copy(): self
+ {
+ return (new self())
+ ->setPointX($this->getPointX())
+ ->setPointY($this->getPointY())
+ ->setPercentageWidth($this->getPercentageWidth())
+ ->setPercentageHeight($this->getPercentageHeight())
+ ->setTitle($this->getTitle())
+ ;
+ }
+
public function getPointX(): int
{
return $this->pointX;
diff --git a/src/Entity/VideoFile.php b/src/Entity/VideoFile.php
index c51221bc..2a8de27e 100644
--- a/src/Entity/VideoFile.php
+++ b/src/Entity/VideoFile.php
@@ -30,7 +30,7 @@ class VideoFile extends AssetFile implements ImagePreviewableInterface
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private Asset $asset;
- #[ORM\OneToMany(mappedBy: 'video', targetEntity: AssetSlot::class, fetch: App::DOCTRINE_EXTRA_LAZY)]
+ #[ORM\OneToMany(targetEntity: AssetSlot::class, mappedBy: 'video', fetch: App::DOCTRINE_EXTRA_LAZY)]
private Collection $slots;
public function __construct()
@@ -41,6 +41,15 @@ public function __construct()
parent::__construct();
}
+ public function __copy(): self
+ {
+ $assetFile = (new self())
+ ->setAttributes(clone $this->getAttributes())
+ ;
+
+ return parent::copyBase($assetFile);
+ }
+
public function getImagePreview(): ?ImagePreview
{
return $this->imagePreview;
diff --git a/src/Helper/UrlHelper.php b/src/Helper/UrlHelper.php
index 6e73a45e..dad60649 100644
--- a/src/Helper/UrlHelper.php
+++ b/src/Helper/UrlHelper.php
@@ -5,9 +5,18 @@
namespace AnzuSystems\CoreDamBundle\Helper;
use AnzuSystems\CoreDamBundle\Model\ValueObject\Url;
+use Symfony\Component\Routing\Requirement\Requirement;
final class UrlHelper
{
+ public static function getImageIdFromUrl(string $url): ?string
+ {
+ $path = (string) parse_url($url, PHP_URL_PATH);
+ preg_match('/^\/image\/w\d+\-h\d+(-c\d+)?(-q\d+)?\/(?' . Requirement::UUID . ')\.jpg$/', $path, $matches);
+
+ return $matches['imageId'] ?? null;
+ }
+
public static function concatPathWithDomain(string $domain, string $path): string
{
if (str_ends_with($domain, '/')) {
diff --git a/src/Messenger/Handler/CopyAssetFileMessageHandler.php b/src/Messenger/Handler/CopyAssetFileMessageHandler.php
new file mode 100644
index 00000000..c75f3532
--- /dev/null
+++ b/src/Messenger/Handler/CopyAssetFileMessageHandler.php
@@ -0,0 +1,53 @@
+assetRepository->find($message->getAssetId());
+ if (null === $asset) {
+ return;
+ }
+ $copyAsset = $this->assetRepository->find($message->getCopyAssetId());
+ if (null === $copyAsset) {
+ return;
+ }
+
+ $this->imageCopyFacade->copyAssetFiles($asset, $copyAsset);
+ }
+}
diff --git a/src/Messenger/Message/CopyAssetFileMessage.php b/src/Messenger/Message/CopyAssetFileMessage.php
new file mode 100644
index 00000000..a7892f95
--- /dev/null
+++ b/src/Messenger/Message/CopyAssetFileMessage.php
@@ -0,0 +1,31 @@
+assetId = (string) $asset->getId();
+ $this->copyAssetId = (string) $copyAsset->getId();
+ }
+
+ public function getAssetId(): string
+ {
+ return $this->assetId;
+ }
+
+ public function getCopyAssetId(): string
+ {
+ return $this->copyAssetId;
+ }
+}
diff --git a/src/Model/Dto/Image/AssetFileCopyResultDto.php b/src/Model/Dto/Image/AssetFileCopyResultDto.php
new file mode 100644
index 00000000..992cdddb
--- /dev/null
+++ b/src/Model/Dto/Image/AssetFileCopyResultDto.php
@@ -0,0 +1,126 @@
+setAsset($asset)
+ ->setTargetAssetLicence($targetAssetLicence)
+ ->setResult($result)
+ ->setTargetAsset($targetAsset)
+ ->setTargetMainFile($targetMainFile)
+ ->setAssetConflicts(new ArrayCollection($assetConflicts))
+ ;
+ }
+
+ public function getTargetAssetLicence(): AssetLicence
+ {
+ return $this->targetAssetLicence;
+ }
+
+ public function setTargetAssetLicence(AssetLicence $targetAssetLicence): self
+ {
+ $this->targetAssetLicence = $targetAssetLicence;
+
+ return $this;
+ }
+
+ public function getAsset(): Asset
+ {
+ return $this->asset;
+ }
+
+ public function setAsset(Asset $asset): self
+ {
+ $this->asset = $asset;
+
+ return $this;
+ }
+
+ #[Serialize(handler: EntityIdHandler::class)]
+ public function getTargetMainFile(): ?AssetFile
+ {
+ return $this->targetMainFile;
+ }
+
+ public function setTargetMainFile(?AssetFile $targetMainFile): self
+ {
+ $this->targetMainFile = $targetMainFile;
+
+ return $this;
+ }
+
+ public function getResult(): AssetFileCopyResult
+ {
+ return $this->result;
+ }
+
+ public function setResult(AssetFileCopyResult $result): self
+ {
+ $this->result = $result;
+
+ return $this;
+ }
+
+ public function getAssetConflicts(): Collection
+ {
+ return $this->assetConflicts;
+ }
+
+ public function setAssetConflicts(Collection $assetConflicts): self
+ {
+ $this->assetConflicts = $assetConflicts;
+
+ return $this;
+ }
+
+ public function getTargetAsset(): ?Asset
+ {
+ return $this->targetAsset;
+ }
+
+ public function setTargetAsset(?Asset $targetAsset): self
+ {
+ $this->targetAsset = $targetAsset;
+
+ return $this;
+ }
+}
diff --git a/src/Model/Dto/Image/ImageCopyDto.php b/src/Model/Dto/Image/ImageCopyDto.php
new file mode 100644
index 00000000..174ad5a9
--- /dev/null
+++ b/src/Model/Dto/Image/ImageCopyDto.php
@@ -0,0 +1,57 @@
+setAsset(new Asset());
+ $this->setTargetAssetLicence(new AssetLicence());
+ }
+
+ public function getTargetAssetLicence(): AssetLicence
+ {
+ return $this->targetAssetLicence;
+ }
+
+ public function setTargetAssetLicence(AssetLicence $targetAssetLicence): self
+ {
+ $this->targetAssetLicence = $targetAssetLicence;
+
+ return $this;
+ }
+
+ public function getAsset(): Asset
+ {
+ return $this->asset;
+ }
+
+ public function setAsset(Asset $asset): self
+ {
+ $this->asset = $asset;
+
+ return $this;
+ }
+}
diff --git a/src/Model/Enum/AssetFileCopyResult.php b/src/Model/Enum/AssetFileCopyResult.php
new file mode 100644
index 00000000..f2a88001
--- /dev/null
+++ b/src/Model/Enum/AssetFileCopyResult.php
@@ -0,0 +1,17 @@
+getAsset()->getLicence()->getExtSystem()->isNot($value->getTargetAssetLicence()->getExtSystem())) {
+ $this->context
+ ->buildViolation(ValidationException::ERROR_INVALID_LICENCE)
+ ->atPath('targetAssetLicence')
+ ->addViolation();
+ }
+ }
+}
diff --git a/tests/Controller/Api/Adm/V1/ImageApiControllerTest.php b/tests/Controller/Api/Adm/V1/ImageApiControllerTest.php
index 2f698c40..8c83fc76 100644
--- a/tests/Controller/Api/Adm/V1/ImageApiControllerTest.php
+++ b/tests/Controller/Api/Adm/V1/ImageApiControllerTest.php
@@ -6,20 +6,22 @@
namespace AnzuSystems\CoreDamBundle\Tests\Controller\Api\Adm\V1;
use AnzuSystems\CoreDamBundle\DataFixtures\AssetLicenceFixtures;
+use AnzuSystems\CoreDamBundle\DataFixtures\AudioFixtures;
+use AnzuSystems\CoreDamBundle\Entity\AssetSlot;
+use AnzuSystems\CoreDamBundle\Entity\AudioFile;
+use AnzuSystems\CoreDamBundle\Entity\ImageFileOptimalResize;
+use AnzuSystems\CoreDamBundle\Exception\ValidationException;
+use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\AssetLicenceFixtures as TestAssetLicenceFixtures;
+use AnzuSystems\CoreDamBundle\Tests\Data\Fixtures\ImageFixtures as TestImageFixtures;
use AnzuSystems\CoreDamBundle\DataFixtures\ImageFixtures;
use AnzuSystems\CoreDamBundle\Domain\Image\ImageUrlFactory;
-use AnzuSystems\CoreDamBundle\Entity\Asset;
-use AnzuSystems\CoreDamBundle\Entity\AssetFile;
-use AnzuSystems\CoreDamBundle\Entity\AssetSlot;
use AnzuSystems\CoreDamBundle\Entity\ImageFile;
use AnzuSystems\CoreDamBundle\Exception\ForbiddenOperationException;
use AnzuSystems\CoreDamBundle\Model\Dto\Asset\AssetAdmDetailDto;
use AnzuSystems\CoreDamBundle\Model\Dto\Image\ImageFileAdmDetailDto;
-use AnzuSystems\CoreDamBundle\Model\Enum\AssetStatus;
use AnzuSystems\CoreDamBundle\Tests\Controller\Api\AbstractAssetFileApiController;
use AnzuSystems\CoreDamBundle\Tests\Data\Entity\User;
use AnzuSystems\CoreDamBundle\Tests\Data\Model\AssetUrl;
-use AnzuSystems\CoreDamBundle\Tests\Data\Model\AssetUrl\AudioUrl;
use AnzuSystems\CoreDamBundle\Tests\Data\Model\AssetUrl\ImageUrl;
use AnzuSystems\SerializerBundle\Exception\SerializerException;
use League\Flysystem\FilesystemException;
@@ -27,7 +29,7 @@
final class ImageApiControllerTest extends AbstractAssetFileApiController
{
- private const TEST_DATA_FILENAME = 'metadata_image.jpeg';
+ private const string TEST_DATA_FILENAME = 'metadata_image.jpeg';
protected ImageUrlFactory $imageUrlFactory;
@@ -242,6 +244,124 @@ public function addChunkFailedDataProvider(): array
];
}
+ public function testCopy(): void
+ {
+ $client = $this->getApiClient(User::ID_ADMIN);
+
+ $imageFile = $this->entityManager->find(ImageFile::class, ImageFixtures::IMAGE_ID_1_1);
+ $duplicateFile = $this->entityManager->find(ImageFile::class, ImageFixtures::IMAGE_ID_3);
+ $conflictFile = $this->entityManager->find(ImageFile::class, ImageFixtures::IMAGE_ID_2);
+
+ $response = $client->patch((
+ new ImageUrl(AssetLicenceFixtures::DEFAULT_LICENCE_ID))->copy(),
+ [
+ [
+ 'asset' => (string) $imageFile->getAsset()->getId(),
+ 'targetAssetLicence' => TestAssetLicenceFixtures::FIRST_SYS_SECONDARY_LICENCE
+ ],
+ [
+ 'asset' => (string) $duplicateFile->getAsset()->getId(),
+ 'targetAssetLicence' => TestAssetLicenceFixtures::FIRST_SYS_SECONDARY_LICENCE
+ ],
+ [
+ 'asset' => (string) $conflictFile->getAsset()->getId(),
+ 'targetAssetLicence' => TestAssetLicenceFixtures::FIRST_SYS_SECONDARY_LICENCE
+ ]
+ ]
+ );
+
+ $this->assertSame(Response::HTTP_OK, $response->getStatusCode());
+ $content = json_decode($response->getContent(), true);
+
+ $copyingItem = array_values(array_filter($content, static fn (array $item) => $item['result'] === 'copying'))[0] ?? null;
+ $this->assertNotNull($copyingItem);
+ if ($copyingItem) {
+ $this->assertSame($copyingItem['asset'], (string) $imageFile->getAsset()->getId());
+ $this->assertNotNull($copyingItem['targetAsset']);
+ $this->assertNotNull($copyingItem['targetMainFile']);
+ $this->assertEmpty($copyingItem['assetConflicts']);
+
+ $copiedImage = $this->entityManager->find(ImageFile::class, $copyingItem['targetMainFile']);
+ $this->assertNotSame($copiedImage->getAssetAttributes()->getFilePath(), $imageFile->getAssetAttributes()->getFilePath());
+ $this->assertCount($imageFile->getResizes()->count(), $copiedImage->getResizes());
+ $this->assertCount($imageFile->getRegionsOfInterest()->count(), $copiedImage->getRegionsOfInterest());
+ foreach ($imageFile->getAsset()->getSlots() as $slot) {
+ /** @var AssetSlot $copiedSlot */
+ $copiedSlot = $copiedImage->getSlots()->findFirst(
+ fn (int $id, AssetSlot $copiedSlot) => $copiedSlot->getName() === $slot->getName()
+ );
+ $this->assertNotNull($copiedSlot);
+ }
+
+ foreach ($imageFile->getResizes() as $resize) {
+ /** @var ImageFileOptimalResize $copiedResize */
+ $copiedResize = $copiedImage->getResizes()->findFirst(
+ fn (int $id, ImageFileOptimalResize $copiedResize) => $copiedResize->getRequestedSize() === $resize->getRequestedSize()
+ );
+ $this->assertNotNull($copiedResize);
+ $this->assertSame($resize->getRequestedSize(), $copiedResize->getRequestedSize());
+ $this->assertSame($resize->getWidth(), $copiedResize->getWidth());
+ $this->assertSame($resize->getHeight(), $copiedResize->getHeight());
+ $this->assertSame($resize->isOriginal(), $copiedResize->isOriginal());
+ $this->assertNotSame($resize->getFilePath(), $copiedResize->getFilePath());
+ }
+
+ $copyImageViewResponse = $client->get('http://image.anzusystems.localhost/image/w800-h450-c0/'.$copyingItem['targetMainFile'].'.jpg');
+ $this->assertSame($copyImageViewResponse->getStatusCode(), Response::HTTP_OK);
+
+ }
+
+ $existsItem = array_values(array_filter($content, static fn (array $item) => $item['result'] === 'exists'))[0] ?? null;
+ $this->assertNotNull($existsItem);
+ if ($existsItem) {
+ $this->assertSame($existsItem['asset'], (string) $duplicateFile->getAsset()->getId());
+ $this->assertNotNull($existsItem['targetAsset']);
+ $this->assertNotNull($existsItem['targetMainFile']);
+
+ $duplicateFileInAnotherLicence = $this->entityManager->find(ImageFile::class, TestImageFixtures::IMAGE_ID_4);
+ $this->assertSame((string) $duplicateFileInAnotherLicence->getAsset()->getId(), $existsItem['targetAsset']);
+ $this->assertSame((string) $duplicateFileInAnotherLicence->getId(), $existsItem['targetMainFile']);
+ $this->assertEmpty($copyingItem['assetConflicts']);
+ }
+
+ $notAllowedItem = array_values(array_filter($content, static fn (array $item) => $item['result'] === 'notAllowed'))[0] ?? null;
+ $this->assertNotNull($notAllowedItem);
+ if ($notAllowedItem) {
+ $this->assertSame($notAllowedItem['asset'], (string) $conflictFile->getAsset()->getId());
+ $this->assertNull($notAllowedItem['targetAsset']);
+ $this->assertNull($notAllowedItem['targetMainFile']);
+ $this->assertNotEmpty($notAllowedItem['assetConflicts']);
+ }
+ }
+
+ public function testCopyFailed(): void
+ {
+ $client = $this->getApiClient(User::ID_ADMIN);
+
+ $imageFile = $this->entityManager->find(ImageFile::class, ImageFixtures::IMAGE_ID_1_1);
+ $audioFile = $this->entityManager->find(AudioFile::class, AudioFixtures::AUDIO_ID_1);
+
+ $response = $client->patch((
+ new ImageUrl(AssetLicenceFixtures::DEFAULT_LICENCE_ID))->copy(),
+ [
+ [
+ 'asset' => (string) $imageFile->getAsset()->getId(),
+ 'targetAssetLicence' => TestAssetLicenceFixtures::LICENCE_2_ID
+ ],
+ [
+ 'asset' => (string) $audioFile->getAsset()->getId(),
+ 'targetAssetLicence' => TestAssetLicenceFixtures::FIRST_SYS_SECONDARY_LICENCE
+ ]
+ ]
+ );
+
+ $this->assertSame(Response::HTTP_UNPROCESSABLE_ENTITY, $response->getStatusCode());
+ $content = json_decode($response->getContent(), true);
+ $this->assertValidationErrors($content, [
+ '[0].targetAssetLicence' => [ValidationException::ERROR_INVALID_LICENCE],
+ '[1].asset' => [ValidationException::ERROR_FIELD_INVALID],
+ ]);
+ }
public function testFinishUploadFailed(): void
{
diff --git a/tests/config/services/services.php b/tests/config/services/services.php
index 4ea9212c..58547c52 100644
--- a/tests/config/services/services.php
+++ b/tests/config/services/services.php
@@ -8,6 +8,7 @@
use AnzuSystems\CommonBundle\Domain\Job\JobManager;
use AnzuSystems\CommonBundle\Exception\Handler\ValidationExceptionHandler;
use AnzuSystems\CoreDamBundle\DataFixtures\AssetLicenceFixtures as BaseAssetLicenceFixtures;
+use AnzuSystems\CoreDamBundle\Domain\AssetFile\AssetFilePositionFacade;
use AnzuSystems\CoreDamBundle\Domain\AssetFile\AssetFileStatusFacadeProvider;
use AnzuSystems\CoreDamBundle\Domain\AssetLicence\AssetLicenceManager;
use AnzuSystems\CoreDamBundle\Domain\AssetLicenceGroup\AssetLicenceGroupManager;
@@ -18,6 +19,7 @@
use AnzuSystems\CoreDamBundle\Domain\ExtSystem\ExtSystemManager;
use AnzuSystems\CoreDamBundle\Domain\Image\ImageFactory;
use AnzuSystems\CoreDamBundle\Domain\Image\ImageManager;
+use AnzuSystems\CoreDamBundle\Domain\Image\ImagePositionFacade;
use AnzuSystems\CoreDamBundle\Domain\User\UserManager;
use AnzuSystems\CoreDamBundle\FileSystem\FileSystemProvider;
use AnzuSystems\CoreDamBundle\Repository\AssetLicenceRepository;
@@ -95,6 +97,7 @@
->arg('$fileSystemProvider', service(FileSystemProvider::class))
->arg('$facadeProvider', service(AssetFileStatusFacadeProvider::class))
->arg('$assetSlotFactory', service(AssetSlotFactory::class))
+ ->arg('$assetFilePositionFacade', service(ImagePositionFacade::class))
->call('setEntityManager', [service(EntityManagerInterface::class)])
->tag(AnzuSystemsCommonBundle::TAG_DATA_FIXTURE);
diff --git a/tests/data/Fixtures/AssetLicenceFixtures.php b/tests/data/Fixtures/AssetLicenceFixtures.php
index 51d4a056..c32d4825 100644
--- a/tests/data/Fixtures/AssetLicenceFixtures.php
+++ b/tests/data/Fixtures/AssetLicenceFixtures.php
@@ -19,6 +19,8 @@ final class AssetLicenceFixtures extends AbstractFixtures
{
public const int LICENCE_ID = BaseAssetLicenceFixtures::DEFAULT_LICENCE_ID + 1;
public const int LICENCE_2_ID = BaseAssetLicenceFixtures::DEFAULT_LICENCE_ID + 2;
+ public const int FIRST_SYS_SECONDARY_LICENCE = BaseAssetLicenceFixtures::DEFAULT_LICENCE_ID + 3;
+
public function __construct(
private readonly AssetLicenceManager $assetLicenceManager,
@@ -70,5 +72,13 @@ private function getData(): Generator
->setId(self::LICENCE_2_ID)
->setExtId('5')
->setExtSystem($blogExtSystem);
+
+ /** @var ExtSystem $cmsExtSystem */
+ $cmsExtSystem = $this->entityManager->find(ExtSystem::class, 1);
+
+ yield (new AssetLicence())
+ ->setId(self::FIRST_SYS_SECONDARY_LICENCE)
+ ->setExtId('2')
+ ->setExtSystem($cmsExtSystem);
}
}
diff --git a/tests/data/Fixtures/ImageFixtures.php b/tests/data/Fixtures/ImageFixtures.php
index ab569d0b..cb2381f6 100644
--- a/tests/data/Fixtures/ImageFixtures.php
+++ b/tests/data/Fixtures/ImageFixtures.php
@@ -5,10 +5,12 @@
namespace AnzuSystems\CoreDamBundle\Tests\Data\Fixtures;
use AnzuSystems\CoreDamBundle\DataFixtures\AbstractAssetFileFixtures;
+use AnzuSystems\CoreDamBundle\Domain\AssetFile\AssetFilePositionFacade;
use AnzuSystems\CoreDamBundle\Domain\AssetFile\AssetFileStatusFacadeProvider;
use AnzuSystems\CoreDamBundle\Domain\AssetSlot\AssetSlotFactory;
use AnzuSystems\CoreDamBundle\Domain\Image\ImageFactory;
use AnzuSystems\CoreDamBundle\Domain\Image\ImageManager;
+use AnzuSystems\CoreDamBundle\Domain\Image\ImagePositionFacade;
use AnzuSystems\CoreDamBundle\Entity\AssetLicence;
use AnzuSystems\CoreDamBundle\Entity\ImageFile;
use AnzuSystems\CoreDamBundle\FileSystem\FileSystemProvider;
@@ -29,6 +31,10 @@ final class ImageFixtures extends AbstractAssetFileFixtures
public const string IMAGE_ID_2_1 = '8e7456dd-80cf-4d09-9ba8-b647d8895358';
public const string IMAGE_ID_3 = '8e7456dd-80cf-4d09-9ba8-b647d8895359';
+ public const string IMAGE_ID_5 = '7d7456dd-80cf-4d09-9ba8-a647d8895345';
+
+ public const string IMAGE_ID_4 = '7d7456dd-80cf-4d09-9ba8-b647d8895345';
+
public function __construct(
private readonly ImageManager $imageManager,
private readonly ImageFactory $imageFactory,
@@ -36,6 +42,7 @@ public function __construct(
private readonly FileSystemProvider $fileSystemProvider,
private readonly AssetFileStatusFacadeProvider $facadeProvider,
private readonly AssetSlotFactory $assetSlotFactory,
+ private readonly ImagePositionFacade $assetFilePositionFacade,
) {
}
@@ -113,5 +120,31 @@ private function getData(): Generator
$this->facadeProvider->getStatusFacade($image)->storeAndProcess($image, $file);
yield $image;
+
+ /** @var AssetLicence $secondaryLicence */
+ $secondaryLicence = $this->licenceRepository->find(AssetLicenceFixtures::FIRST_SYS_SECONDARY_LICENCE);
+
+ $file = $this->getFile($fileSystem, 'text_image_192x108.jpg');
+ $image = $this->imageFactory->createFromFile(
+ $file,
+ $secondaryLicence,
+ self::IMAGE_ID_5
+ );
+ $image->getAssetAttributes()->setStatus(AssetFileProcessStatus::Uploaded);
+ $this->facadeProvider->getStatusFacade($image)->storeAndProcess($image, $file);
+ $this->assetFilePositionFacade->setToSlot($image->getAsset(), $image, 'free');
+
+ yield $image;
+
+ $file = $this->getFile($fileSystem, 'text_image_200x200.jpg');
+ $image = $this->imageFactory->createFromFile(
+ $file,
+ $secondaryLicence,
+ self::IMAGE_ID_4
+ );
+ $image->getAssetAttributes()->setStatus(AssetFileProcessStatus::Uploaded);
+ $this->facadeProvider->getStatusFacade($image)->storeAndProcess($image, $file);
+
+ yield $image;
}
}
diff --git a/tests/data/Model/AssetUrl/ImageUrl.php b/tests/data/Model/AssetUrl/ImageUrl.php
index 02faa2be..b3bdc12d 100644
--- a/tests/data/Model/AssetUrl/ImageUrl.php
+++ b/tests/data/Model/AssetUrl/ImageUrl.php
@@ -43,6 +43,11 @@ public function setMainFilePath(string $assetId, string $imageId): string
return "/api/adm/v{$this->version}/image/{$imageId}/asset/{$assetId}/main";
}
+ public function copy(): string
+ {
+ return "/api/adm/v{$this->version}/image/copy-to-licence";
+ }
+
public function getSerializeClassString(): string
{
return ImageFileAdmDetailDto::class;