Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions Neos.ContentRepository.Export/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "neos/contentrepository-export",
"description": "Import/Export functionality for the Event Sourced Content Repository",
"type": "library",
"require": {
"php": ">=8.1",
"neos/content-repository": "^8.0",
"league/flysystem": "^3",
"webmozart/assert": "^1.11"
},
"require-dev": {
"roave/security-advisories": "dev-latest",
"phpstan/phpstan": "^1.8"
},
"suggest": {
"league/flysystem-ziparchive": "to export zip archives",
"neos/media": "to import Assets",
"neos/escr-asset-usage": "to export used assets"
},
"license": "MIT",
"autoload": {
"psr-4": {
"Neos\\ContentRepository\\Export\\": "src/"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Neos\ContentRepository\Export\Asset\Adapters;

use Neos\ContentRepository\Export\Asset\AssetLoaderInterface;
use Neos\ContentRepository\Export\Asset\ValueObject\SerializedAsset;
use Neos\ContentRepository\Export\Asset\ValueObject\SerializedImageVariant;
use Neos\Media\Domain\Model\Asset;
use Neos\Media\Domain\Model\ImageVariant;
use Neos\Media\Domain\Repository\AssetRepository;

final class AssetRepositoryAssetLoader implements AssetLoaderInterface
{
public function __construct(
private readonly AssetRepository $assetRepository,
) {}

public function findAssetById(string $assetId): SerializedAsset|SerializedImageVariant
{
$asset = $this->assetRepository->findByIdentifier($assetId);
if ($asset === null) {
throw new \InvalidArgumentException(sprintf('Failed to load asset with id "%s"', $assetId), 1658652322);
}
if ($asset instanceof ImageVariant) {
return SerializedImageVariant::fromImageVariant($asset);
}
if (!$asset instanceof Asset) {
throw new \RuntimeException(sprintf('Asset "%s" was expected to be of type "%s" bit it is a "%s"', $assetId, Asset::class, get_debug_type($asset)), 1658652326);
}
return SerializedAsset::fromAsset($asset);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Neos\ContentRepository\Export\Asset\Adapters;

use Doctrine\DBAL\Connection;
use Neos\ContentRepository\Export\Asset\AssetLoaderInterface;
use Neos\ContentRepository\Export\Asset\ValueObject\AssetType;
use Neos\ContentRepository\Export\Asset\ValueObject\ImageAdjustmentType;
use Neos\ContentRepository\Export\Asset\ValueObject\SerializedAsset;
use Neos\ContentRepository\Export\Asset\ValueObject\SerializedImageVariant;


final class DbalAssetLoader implements AssetLoaderInterface
{
public function __construct(
private readonly Connection $connection,
) {}

public function findAssetById(string $assetId): SerializedAsset|SerializedImageVariant
{
$row = $this->connection->fetchAssociative('
SELECT
a.persistence_object_identifier identifier,
a.dtype type,
a.title,
a.copyrightNotice,
a.caption,
a.assetSourceIdentifier,
r.filename resource_filename,
r.collectionName resource_collectionName,
r.mediaType resource_mediaType,
r.sha1 resource_sha1,
v.originalasset originalAssetIdentifier,
v.name,
v.width,
v.height,
v.presetIdentifier,
v.presetVariantName
FROM
neos_media_domain_model_asset a
INNER JOIN
neos_flow_resourcemanagement_persistentresource r ON r.persistence_object_identifier = a.resource
LEFT JOIN
neos_media_domain_model_imagevariant v ON v.persistence_object_identifier = a.persistence_object_identifier
WHERE
a.persistence_object_identifier = :assetId',
['assetId' => $assetId]
);
if ($row === false) {
throw new \InvalidArgumentException(sprintf('Failed to load asset with id "%s"', $assetId), 1658495421);
}
if ($row['originalAssetIdentifier'] !== null) {
$imageAdjustmentRows = $this->connection->fetchAllAssociative('SELECT * FROM neos_media_domain_model_adjustment_abstractimageadjustment WHERE imagevariant = :assetId ORDER BY position', ['assetId' => $assetId]);
$imageAdjustments = [];
foreach ($imageAdjustmentRows as $imageAdjustmentRow) {
$type = match ($imageAdjustmentRow['dtype']) {
'neos_media_adjustment_resizeimageadjustment' => ImageAdjustmentType::RESIZE_IMAGE,
'neos_media_adjustment_cropimageadjustment' => ImageAdjustmentType::CROP_IMAGE,
'neos_media_adjustment_qualityimageadjustment' => ImageAdjustmentType::QUALITY_IMAGE,
};
#unset($imageAdjustmentRow['persistence_object_identifier'], $imageAdjustmentRow['imagevariant'], $imageAdjustmentRow['dtype']);
#$imageAdjustmentRow = array_filter($imageAdjustmentRow, static fn ($value) => $value !== null);
$imageAdjustments[] = ['type' => $type->value, 'properties' => $type->convertProperties($imageAdjustmentRow)];
}
return SerializedImageVariant::fromArray([
'identifier' => $row['identifier'],
'originalAssetIdentifier' => $row['originalAssetIdentifier'],
'name' => $row['name'],
'width' => $row['width'],
'height' => $row['height'],
'presetIdentifier' => $row['presetIdentifier'],
'presetVariantName' => $row['presetVariantName'],
'imageAdjustments' => $imageAdjustments
]);
}
$row = array_filter($row, static fn ($value) => $value !== null);
foreach ($row as $key => $value) {
if (!str_starts_with($key, 'resource_')) {
continue;
}
$row['resource'][substr($key, 9)] = $value;
unset($row[$key]);
}
$row['type'] = match ($row['type']) {
'neos_media_image' => AssetType::IMAGE->value,
'neos_media_audio' => AssetType::AUDIO->value,
'neos_media_document' => AssetType::DOCUMENT->value,
'neos_media_video' => AssetType::VIDEO->value,
};
return SerializedAsset::fromArray($row);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Neos\ContentRepository\Export\Asset\Adapters;

use Neos\ContentRepository\Export\Asset\ResourceLoaderInterface;
use Neos\Utility\Files;

final class FileSystemResourceLoader implements ResourceLoaderInterface
{

public function __construct(
private readonly string $path,
) {}

public function getStreamBySha1(string $sha1)
{
if (strlen($sha1) < 5) {
throw new \InvalidArgumentException(sprintf('Specified SHA1 "%s" is too short', $sha1), 1658583570);
}
$resourcePath = Files::concatenatePaths([$this->path, $sha1[0], $sha1[1], $sha1[2], $sha1[3], $sha1]);
if (!is_readable($resourcePath)) {
throw new \RuntimeException(sprintf('Resource file "%s" is not readable', $resourcePath), 1658583621);
}
return fopen($resourcePath, 'rb');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Neos\ContentRepository\Export\Asset\Adapters;

use Neos\ContentRepository\Export\Asset\ResourceLoaderInterface;
use Neos\Flow\ResourceManagement\ResourceManager;

final class ResourceManagerResourceLoader implements ResourceLoaderInterface
{

public function __construct(
private readonly ResourceManager $resourceManager,
) {}

public function getStreamBySha1(string $sha1)
{
$resource = $this->resourceManager->getResourceBySha1($sha1);
if ($resource === null) {
throw new \InvalidArgumentException(sprintf('Failed to find resource for SHA1 "%s"', $sha1), 1658583711);
}
$stream = $this->resourceManager->getStreamByResource($resource);
if (!is_resource($stream)) {
throw new \RuntimeException(sprintf('Failed to load file for persistent resource with SHA1 "%s"', $sha1), 1658583763);
}
return $stream;
}
}
38 changes: 38 additions & 0 deletions Neos.ContentRepository.Export/src/Asset/AssetExporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Neos\ContentRepository\Export\Asset;

use League\Flysystem\Filesystem;
use Neos\ContentRepository\Export\Asset\ValueObject\SerializedImageVariant;

final class AssetExporter
{
private array $exportedAssetIds = [];

public function __construct(
private readonly Filesystem $files,
private readonly AssetLoaderInterface $assetLoader,
private readonly ResourceLoaderInterface $resourceLoader,
) {}

public function exportAsset(string $assetId): void
{
if (array_key_exists($assetId, $this->exportedAssetIds)) {
return;
}
$serializedAsset = $this->assetLoader->findAssetById($assetId);
$this->exportedAssetIds[$assetId] = true;
if ($serializedAsset instanceof SerializedImageVariant) {
$this->files->write('ImageVariants/' . $serializedAsset->identifier . '.json', $serializedAsset->toJson());
$this->exportAsset($serializedAsset->originalAssetIdentifier);
return;
}
try {
$resourceStream = $this->resourceLoader->getStreamBySha1($serializedAsset->resource->sha1);
} catch (\Exception $e) {
throw new \RuntimeException(sprintf('Failed to find resource with SHA1 "%s", referenced in asset "%s": %s', $serializedAsset->resource->sha1, $serializedAsset->identifier, $e->getMessage()), 1658495163, $e);
}
$this->files->write('Assets/' . $serializedAsset->identifier . '.json', $serializedAsset->toJson());
$this->files->writeStream('Resources/' . $serializedAsset->resource->sha1, $resourceStream);
}
}
11 changes: 11 additions & 0 deletions Neos.ContentRepository.Export/src/Asset/AssetLoaderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Neos\ContentRepository\Export\Asset;

use Neos\ContentRepository\Export\Asset\ValueObject\SerializedAsset;
use Neos\ContentRepository\Export\Asset\ValueObject\SerializedImageVariant;

interface AssetLoaderInterface
{
public function findAssetById(string $assetId): SerializedAsset|SerializedImageVariant;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Neos\ContentRepository\Export\Asset;

interface ResourceLoaderInterface
{
/**
* @param string $sha1
* @return resource
*/
public function getStreamBySha1(string $sha1);
}
11 changes: 11 additions & 0 deletions Neos.ContentRepository.Export/src/Asset/ValueObject/AssetType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Neos\ContentRepository\Export\Asset\ValueObject;

enum AssetType: string
{
case IMAGE = 'IMAGE';
case AUDIO = 'AUDIO';
case DOCUMENT = 'DOCUMENT';
case VIDEO = 'VIDEO';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Neos\ContentRepository\Export\Asset\ValueObject;

enum ImageAdjustmentType: string
{
case RESIZE_IMAGE = 'RESIZE_IMAGE';
case CROP_IMAGE = 'CROP_IMAGE';
case QUALITY_IMAGE = 'QUALITY_IMAGE';

public function convertProperties(array $data): array
{
$data = array_change_key_case($data);
$convertedData = match ($this) {
self::RESIZE_IMAGE => [
'width' => isset($data['width']) ? (int)$data['width'] : null,
'height' => isset($data['height']) ? (int)$data['height'] : null,
'maximumWidth' => isset($data['maximumwidth']) ? (int)$data['maximumwidth'] : null,
'maximumHeight' => isset($data['maximumheight']) ? (int)$data['maximumheight'] : null,
'minimumWidth' => isset($data['minimumwidth']) ? (int)$data['minimumwidth'] : null,
'minimumHeight' => isset($data['minimumheight']) ? (int)$data['minimumheight'] : null,
'ratioMode' => isset($data['ratiomode']) && in_array($data['ratiomode'], ['inset', 'outbound'], true) ? $data['ratiomode'] : null,
'allowUpScaling' => isset($data['allowupscaling']) ? (bool)$data['allowupscaling'] : null,
'filter' => isset($data['filter']) ? (string)$data['filter'] : null,
],
self::CROP_IMAGE => [
'x' => isset($data['x']) ? (int)$data['x'] : null,
'y' => isset($data['y']) ? (int)$data['y'] : null,
'width' => isset($data['width']) ? (int)$data['width'] : null,
'height' => isset($data['height']) ? (int)$data['height'] : null,
'aspectRatioAsString' => isset($data['aspectratioasstring']) ? (string)$data['aspectratioasstring'] : null,
],
self::QUALITY_IMAGE => [
'quality' => isset($data['quality']) ? (int)$data['quality'] : null,
]
};
return array_filter($convertedData, static fn ($value) => $value !== null);
}
}
Loading