commit
a991225c04
17 changed files with 2353 additions and 0 deletions
@ -0,0 +1,6 @@ |
|||||||
|
storage_root: 'private://ocfl-storage' |
||||||
|
auto_export_enabled: true |
||||||
|
content_types: [] |
||||||
|
auto_export_media_enabled: true |
||||||
|
media_types: [] |
||||||
|
ocfl_version: '1.1' |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
langcode: en |
||||||
|
status: true |
||||||
|
dependencies: |
||||||
|
module: |
||||||
|
- media |
||||||
|
- ocfl_export |
||||||
|
id: ocfl_export_media |
||||||
|
label: 'Export to OCFL' |
||||||
|
type: media |
||||||
|
plugin: ocfl_export_media |
||||||
|
configuration: {} |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
langcode: en |
||||||
|
status: true |
||||||
|
dependencies: |
||||||
|
module: |
||||||
|
- node |
||||||
|
- ocfl_export |
||||||
|
id: ocfl_export_node |
||||||
|
label: 'Export to OCFL' |
||||||
|
type: node |
||||||
|
plugin: ocfl_export_node |
||||||
|
configuration: {} |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
ocfl_export.settings: |
||||||
|
type: config_object |
||||||
|
label: 'OCFL Export settings' |
||||||
|
mapping: |
||||||
|
storage_root: |
||||||
|
type: string |
||||||
|
label: 'Storage root path' |
||||||
|
auto_export_enabled: |
||||||
|
type: boolean |
||||||
|
label: 'Enable automatic export on node save' |
||||||
|
content_types: |
||||||
|
type: sequence |
||||||
|
label: 'Content types to export' |
||||||
|
sequence: |
||||||
|
type: string |
||||||
|
label: 'Content type machine name' |
||||||
|
auto_export_media_enabled: |
||||||
|
type: boolean |
||||||
|
label: 'Enable automatic export on media save' |
||||||
|
media_types: |
||||||
|
type: sequence |
||||||
|
label: 'Media types to export' |
||||||
|
sequence: |
||||||
|
type: string |
||||||
|
label: 'Media type machine name' |
||||||
|
ocfl_version: |
||||||
|
type: string |
||||||
|
label: 'OCFL version' |
||||||
@ -0,0 +1,11 @@ |
|||||||
|
name: 'OCFL Export' |
||||||
|
type: module |
||||||
|
description: 'Exports nodes and media as OCFL (Oxford Common File Layout) objects on the filesystem.' |
||||||
|
package: Content |
||||||
|
core_version_requirement: ^10.5 |
||||||
|
php: 8.3 |
||||||
|
dependencies: |
||||||
|
- drupal:node |
||||||
|
- drupal:user |
||||||
|
- drupal:media |
||||||
|
configure: ocfl_export.settings |
||||||
@ -0,0 +1,13 @@ |
|||||||
|
ocfl_export.export_node: |
||||||
|
route_name: ocfl_export.export_node |
||||||
|
title: 'Export to OCFL' |
||||||
|
appears_on: |
||||||
|
- entity.node.canonical |
||||||
|
- entity.node.edit_form |
||||||
|
|
||||||
|
ocfl_export.export_media: |
||||||
|
route_name: ocfl_export.export_media |
||||||
|
title: 'Export to OCFL' |
||||||
|
appears_on: |
||||||
|
- entity.media.canonical |
||||||
|
- entity.media.edit_form |
||||||
@ -0,0 +1,264 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
/** |
||||||
|
* @file |
||||||
|
* OCFL Export module hooks and functions. |
||||||
|
*/ |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
use Drupal\Core\Entity\Display\EntityViewDisplayInterface; |
||||||
|
use Drupal\Core\Entity\EntityInterface; |
||||||
|
use Drupal\Core\Url; |
||||||
|
use Drupal\media\MediaInterface; |
||||||
|
use Drupal\node\NodeInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements hook_node_insert(). |
||||||
|
*/ |
||||||
|
function ocfl_export_node_insert(NodeInterface $node): void { |
||||||
|
_ocfl_export_save_node($node); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements hook_node_update(). |
||||||
|
*/ |
||||||
|
function ocfl_export_node_update(NodeInterface $node): void { |
||||||
|
_ocfl_export_save_node($node); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements hook_node_delete(). |
||||||
|
*/ |
||||||
|
function ocfl_export_node_delete(NodeInterface $node): void { |
||||||
|
$config = \Drupal::config('ocfl_export.settings'); |
||||||
|
|
||||||
|
if (!$config->get('auto_export_enabled')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$content_types = $config->get('content_types') ?: []; |
||||||
|
if (!empty($content_types) && !in_array($node->bundle(), $content_types, TRUE)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
/** @var \Drupal\ocfl_export\OcflStorageService $storage */ |
||||||
|
$storage = \Drupal::service('ocfl_export.storage'); |
||||||
|
$storage->markDeleted($node); |
||||||
|
} |
||||||
|
catch (\Exception $e) { |
||||||
|
\Drupal::logger('ocfl_export')->error('Failed to mark OCFL object as deleted for node @nid: @message', [ |
||||||
|
'@nid' => $node->id(), |
||||||
|
'@message' => $e->getMessage(), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Helper function to save node to OCFL storage. |
||||||
|
*/ |
||||||
|
function _ocfl_export_save_node(NodeInterface $node): void { |
||||||
|
$config = \Drupal::config('ocfl_export.settings'); |
||||||
|
|
||||||
|
if (!$config->get('auto_export_enabled')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$content_types = $config->get('content_types') ?: []; |
||||||
|
if (!empty($content_types) && !in_array($node->bundle(), $content_types, TRUE)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
/** @var \Drupal\ocfl_export\NodeSerializerService $serializer */ |
||||||
|
$serializer = \Drupal::service('ocfl_export.node_serializer'); |
||||||
|
/** @var \Drupal\ocfl_export\OcflStorageService $storage */ |
||||||
|
$storage = \Drupal::service('ocfl_export.storage'); |
||||||
|
|
||||||
|
$data = $serializer->serialize($node); |
||||||
|
$storage->saveObject($node->uuid(), $data); |
||||||
|
} |
||||||
|
catch (\Exception $e) { |
||||||
|
\Drupal::logger('ocfl_export')->error('Failed to export node @nid to OCFL: @message', [ |
||||||
|
'@nid' => $node->id(), |
||||||
|
'@message' => $e->getMessage(), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements hook_media_insert(). |
||||||
|
*/ |
||||||
|
function ocfl_export_media_insert(MediaInterface $media): void { |
||||||
|
_ocfl_export_save_media($media); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements hook_media_update(). |
||||||
|
*/ |
||||||
|
function ocfl_export_media_update(MediaInterface $media): void { |
||||||
|
_ocfl_export_save_media($media); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements hook_media_delete(). |
||||||
|
*/ |
||||||
|
function ocfl_export_media_delete(MediaInterface $media): void { |
||||||
|
$config = \Drupal::config('ocfl_export.settings'); |
||||||
|
|
||||||
|
if (!$config->get('auto_export_media_enabled')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$media_types = $config->get('media_types') ?: []; |
||||||
|
if (!empty($media_types) && !in_array($media->bundle(), $media_types, TRUE)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
/** @var \Drupal\ocfl_export\OcflStorageService $storage */ |
||||||
|
$storage = \Drupal::service('ocfl_export.storage'); |
||||||
|
$storage->markMediaDeleted($media); |
||||||
|
} |
||||||
|
catch (\Exception $e) { |
||||||
|
\Drupal::logger('ocfl_export')->error('Failed to mark OCFL object as deleted for media @mid: @message', [ |
||||||
|
'@mid' => $media->id(), |
||||||
|
'@message' => $e->getMessage(), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Helper function to save media to OCFL storage. |
||||||
|
*/ |
||||||
|
function _ocfl_export_save_media(MediaInterface $media): void { |
||||||
|
$config = \Drupal::config('ocfl_export.settings'); |
||||||
|
|
||||||
|
if (!$config->get('auto_export_media_enabled')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$media_types = $config->get('media_types') ?: []; |
||||||
|
if (!empty($media_types) && !in_array($media->bundle(), $media_types, TRUE)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
/** @var \Drupal\ocfl_export\MediaSerializerService $serializer */ |
||||||
|
$serializer = \Drupal::service('ocfl_export.media_serializer'); |
||||||
|
/** @var \Drupal\ocfl_export\OcflStorageService $storage */ |
||||||
|
$storage = \Drupal::service('ocfl_export.storage'); |
||||||
|
|
||||||
|
$data = $serializer->serialize($media); |
||||||
|
$storage->saveObject($media->uuid(), $data); |
||||||
|
} |
||||||
|
catch (\Exception $e) { |
||||||
|
\Drupal::logger('ocfl_export')->error('Failed to export media @mid to OCFL: @message', [ |
||||||
|
'@mid' => $media->id(), |
||||||
|
'@message' => $e->getMessage(), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements hook_node_view(). |
||||||
|
*/ |
||||||
|
function ocfl_export_node_view(array &$build, NodeInterface $node, EntityViewDisplayInterface $display, string $view_mode): void { |
||||||
|
if ($view_mode !== 'full') { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!\Drupal::currentUser()->hasPermission('export ocfl content')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$build['ocfl_export_status'] = _ocfl_export_build_status_element($node->uuid(), 'node', $node->id()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Implements hook_media_view(). |
||||||
|
*/ |
||||||
|
function ocfl_export_media_view(array &$build, MediaInterface $media, EntityViewDisplayInterface $display, string $view_mode): void { |
||||||
|
if ($view_mode !== 'full') { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!\Drupal::currentUser()->hasPermission('export ocfl content')) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
$build['ocfl_export_status'] = _ocfl_export_build_status_element($media->uuid(), 'media', $media->id()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Build the OCFL export status render element. |
||||||
|
* |
||||||
|
* @param string $uuid |
||||||
|
* The entity UUID. |
||||||
|
* @param string $entity_type |
||||||
|
* The entity type ('node' or 'media'). |
||||||
|
* @param int|string $entity_id |
||||||
|
* The entity ID. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* A render array for the OCFL export status. |
||||||
|
*/ |
||||||
|
function _ocfl_export_build_status_element(string $uuid, string $entity_type, int|string $entity_id): array { |
||||||
|
/** @var \Drupal\ocfl_export\OcflStorageService $storage */ |
||||||
|
$storage = \Drupal::service('ocfl_export.storage'); |
||||||
|
$status = $storage->getExportStatus($uuid); |
||||||
|
|
||||||
|
$element = [ |
||||||
|
'#type' => 'container', |
||||||
|
'#attributes' => [ |
||||||
|
'class' => ['ocfl-export-status'], |
||||||
|
], |
||||||
|
'#weight' => -100, |
||||||
|
]; |
||||||
|
|
||||||
|
if ($status) { |
||||||
|
$element['info'] = [ |
||||||
|
'#type' => 'html_tag', |
||||||
|
'#tag' => 'div', |
||||||
|
'#attributes' => [ |
||||||
|
'class' => ['ocfl-export-info'], |
||||||
|
], |
||||||
|
'#value' => t('OCFL Export: @version (exported @date)', [ |
||||||
|
'@version' => $status['version'], |
||||||
|
'@date' => $status['created'] ? \Drupal::service('date.formatter')->format(strtotime($status['created']), 'short') : t('unknown'), |
||||||
|
]), |
||||||
|
]; |
||||||
|
|
||||||
|
$element['link'] = [ |
||||||
|
'#type' => 'html_tag', |
||||||
|
'#tag' => 'div', |
||||||
|
'#attributes' => [ |
||||||
|
'class' => ['ocfl-export-link'], |
||||||
|
], |
||||||
|
'content' => [ |
||||||
|
'#type' => 'link', |
||||||
|
'#title' => t('View exported JSON file'), |
||||||
|
'#url' => Url::fromRoute('ocfl_export.view_export', [ |
||||||
|
'entity_type' => $entity_type, |
||||||
|
'uuid' => $uuid, |
||||||
|
]), |
||||||
|
'#attributes' => [ |
||||||
|
'target' => '_blank', |
||||||
|
], |
||||||
|
], |
||||||
|
]; |
||||||
|
} |
||||||
|
else { |
||||||
|
$element['info'] = [ |
||||||
|
'#type' => 'html_tag', |
||||||
|
'#tag' => 'div', |
||||||
|
'#attributes' => [ |
||||||
|
'class' => ['ocfl-export-info', 'ocfl-not-exported'], |
||||||
|
], |
||||||
|
'#value' => t('Not yet exported to OCFL'), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
return $element; |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
administer ocfl export: |
||||||
|
title: 'Administer OCFL Export' |
||||||
|
description: 'Configure OCFL export settings including storage path and content types.' |
||||||
|
restrict access: true |
||||||
|
|
||||||
|
export ocfl content: |
||||||
|
title: 'Export content to OCFL' |
||||||
|
description: 'Manually export nodes to OCFL storage.' |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
ocfl_export.settings: |
||||||
|
path: '/admin/config/content/ocfl-export' |
||||||
|
defaults: |
||||||
|
_form: '\Drupal\ocfl_export\Form\OcflSettingsForm' |
||||||
|
_title: 'OCFL Export Settings' |
||||||
|
requirements: |
||||||
|
_permission: 'administer ocfl export' |
||||||
|
|
||||||
|
ocfl_export.export_node: |
||||||
|
path: '/admin/content/ocfl-export/{node}' |
||||||
|
defaults: |
||||||
|
_controller: '\Drupal\ocfl_export\Controller\OcflExportController::exportNode' |
||||||
|
_title: 'Export to OCFL' |
||||||
|
requirements: |
||||||
|
_permission: 'export ocfl content' |
||||||
|
options: |
||||||
|
parameters: |
||||||
|
node: |
||||||
|
type: entity:node |
||||||
|
|
||||||
|
ocfl_export.export_media: |
||||||
|
path: '/admin/content/ocfl-export-media/{media}' |
||||||
|
defaults: |
||||||
|
_controller: '\Drupal\ocfl_export\Controller\OcflExportController::exportMedia' |
||||||
|
_title: 'Export Media to OCFL' |
||||||
|
requirements: |
||||||
|
_permission: 'export ocfl content' |
||||||
|
options: |
||||||
|
parameters: |
||||||
|
media: |
||||||
|
type: entity:media |
||||||
|
|
||||||
|
ocfl_export.view_export: |
||||||
|
path: '/admin/content/ocfl-view/{entity_type}/{uuid}' |
||||||
|
defaults: |
||||||
|
_controller: '\Drupal\ocfl_export\Controller\OcflExportController::viewExport' |
||||||
|
_title: 'View OCFL Export' |
||||||
|
requirements: |
||||||
|
_permission: 'export ocfl content' |
||||||
|
entity_type: 'node|media' |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
services: |
||||||
|
ocfl_export.storage: |
||||||
|
class: Drupal\ocfl_export\OcflStorageService |
||||||
|
arguments: |
||||||
|
- '@config.factory' |
||||||
|
- '@file_system' |
||||||
|
- '@logger.factory' |
||||||
|
|
||||||
|
ocfl_export.node_serializer: |
||||||
|
class: Drupal\ocfl_export\NodeSerializerService |
||||||
|
arguments: |
||||||
|
- '@entity_type.manager' |
||||||
|
- '@datetime.time' |
||||||
|
|
||||||
|
ocfl_export.media_serializer: |
||||||
|
class: Drupal\ocfl_export\MediaSerializerService |
||||||
|
arguments: |
||||||
|
- '@entity_type.manager' |
||||||
|
- '@datetime.time' |
||||||
@ -0,0 +1,163 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Drupal\ocfl_export\Controller; |
||||||
|
|
||||||
|
use Drupal\Core\Controller\ControllerBase; |
||||||
|
use Drupal\Core\Messenger\MessengerInterface; |
||||||
|
use Drupal\Core\Url; |
||||||
|
use Drupal\media\MediaInterface; |
||||||
|
use Drupal\node\NodeInterface; |
||||||
|
use Drupal\ocfl_export\MediaSerializerService; |
||||||
|
use Drupal\ocfl_export\NodeSerializerService; |
||||||
|
use Drupal\ocfl_export\OcflStorageService; |
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse; |
||||||
|
use Symfony\Component\HttpFoundation\Response; |
||||||
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Controller for manual OCFL export operations. |
||||||
|
*/ |
||||||
|
class OcflExportController extends ControllerBase { |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
protected OcflStorageService $storageService, |
||||||
|
protected NodeSerializerService $nodeSerializerService, |
||||||
|
protected MediaSerializerService $mediaSerializerService, |
||||||
|
MessengerInterface $messenger, |
||||||
|
) { |
||||||
|
$this->messenger = $messenger; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function create(ContainerInterface $container): static { |
||||||
|
return new static( |
||||||
|
$container->get('ocfl_export.storage'), |
||||||
|
$container->get('ocfl_export.node_serializer'), |
||||||
|
$container->get('ocfl_export.media_serializer'), |
||||||
|
$container->get('messenger'), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Export a node to OCFL storage. |
||||||
|
* |
||||||
|
* @param \Drupal\node\NodeInterface $node |
||||||
|
* The node to export. |
||||||
|
* |
||||||
|
* @return \Symfony\Component\HttpFoundation\RedirectResponse |
||||||
|
* A redirect response back to the node. |
||||||
|
*/ |
||||||
|
public function exportNode(NodeInterface $node): RedirectResponse { |
||||||
|
try { |
||||||
|
$data = $this->nodeSerializerService->serialize($node); |
||||||
|
$success = $this->storageService->saveObject($node->uuid(), $data); |
||||||
|
|
||||||
|
if ($success) { |
||||||
|
$this->messenger->addStatus($this->t('Node "@title" has been exported to OCFL storage.', [ |
||||||
|
'@title' => $node->getTitle(), |
||||||
|
])); |
||||||
|
|
||||||
|
// Get object info for additional feedback. |
||||||
|
$objectInfo = $this->storageService->getObjectInfo($node->uuid()); |
||||||
|
if ($objectInfo) { |
||||||
|
$this->messenger->addStatus($this->t('OCFL object version: @version', [ |
||||||
|
'@version' => $objectInfo['head'] ?? 'unknown', |
||||||
|
])); |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
$this->messenger->addError($this->t('Failed to export node "@title" to OCFL storage.', [ |
||||||
|
'@title' => $node->getTitle(), |
||||||
|
])); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (\Exception $e) { |
||||||
|
$this->messenger->addError($this->t('An error occurred while exporting: @message', [ |
||||||
|
'@message' => $e->getMessage(), |
||||||
|
])); |
||||||
|
$this->getLogger('ocfl_export')->error('Export failed for node @nid: @message', [ |
||||||
|
'@nid' => $node->id(), |
||||||
|
'@message' => $e->getMessage(), |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
return new RedirectResponse(Url::fromRoute('entity.node.canonical', ['node' => $node->id()])->toString()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Export a media entity to OCFL storage. |
||||||
|
* |
||||||
|
* @param \Drupal\media\MediaInterface $media |
||||||
|
* The media entity to export. |
||||||
|
* |
||||||
|
* @return \Symfony\Component\HttpFoundation\RedirectResponse |
||||||
|
* A redirect response back to the media entity. |
||||||
|
*/ |
||||||
|
public function exportMedia(MediaInterface $media): RedirectResponse { |
||||||
|
try { |
||||||
|
$data = $this->mediaSerializerService->serialize($media); |
||||||
|
$success = $this->storageService->saveObject($media->uuid(), $data); |
||||||
|
|
||||||
|
if ($success) { |
||||||
|
$this->messenger->addStatus($this->t('Media "@name" has been exported to OCFL storage.', [ |
||||||
|
'@name' => $media->getName(), |
||||||
|
])); |
||||||
|
|
||||||
|
// Get object info for additional feedback. |
||||||
|
$objectInfo = $this->storageService->getObjectInfo($media->uuid()); |
||||||
|
if ($objectInfo) { |
||||||
|
$this->messenger->addStatus($this->t('OCFL object version: @version', [ |
||||||
|
'@version' => $objectInfo['head'] ?? 'unknown', |
||||||
|
])); |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
$this->messenger->addError($this->t('Failed to export media "@name" to OCFL storage.', [ |
||||||
|
'@name' => $media->getName(), |
||||||
|
])); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (\Exception $e) { |
||||||
|
$this->messenger->addError($this->t('An error occurred while exporting: @message', [ |
||||||
|
'@message' => $e->getMessage(), |
||||||
|
])); |
||||||
|
$this->getLogger('ocfl_export')->error('Export failed for media @mid: @message', [ |
||||||
|
'@mid' => $media->id(), |
||||||
|
'@message' => $e->getMessage(), |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
return new RedirectResponse(Url::fromRoute('entity.media.canonical', ['media' => $media->id()])->toString()); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* View the exported JSON file for an entity. |
||||||
|
* |
||||||
|
* @param string $entity_type |
||||||
|
* The entity type ('node' or 'media'). |
||||||
|
* @param string $uuid |
||||||
|
* The entity UUID. |
||||||
|
* |
||||||
|
* @return \Symfony\Component\HttpFoundation\Response |
||||||
|
* A response containing the JSON content. |
||||||
|
*/ |
||||||
|
public function viewExport(string $entity_type, string $uuid): Response { |
||||||
|
$filePath = $this->storageService->getLatestExportPath($uuid); |
||||||
|
|
||||||
|
if (!$filePath || !file_exists($filePath)) { |
||||||
|
throw new NotFoundHttpException('OCFL export not found for this entity.'); |
||||||
|
} |
||||||
|
|
||||||
|
$content = file_get_contents($filePath); |
||||||
|
|
||||||
|
return new Response($content, 200, [ |
||||||
|
'Content-Type' => 'application/json', |
||||||
|
]); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,222 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Drupal\ocfl_export\Form; |
||||||
|
|
||||||
|
use Drupal\Core\Config\ConfigFactoryInterface; |
||||||
|
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||||
|
use Drupal\Core\Form\ConfigFormBase; |
||||||
|
use Drupal\Core\Form\FormStateInterface; |
||||||
|
use Drupal\ocfl_export\OcflStorageService; |
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Configuration form for OCFL Export settings. |
||||||
|
*/ |
||||||
|
class OcflSettingsForm extends ConfigFormBase { |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
ConfigFactoryInterface $config_factory, |
||||||
|
protected EntityTypeManagerInterface $entityTypeManager, |
||||||
|
protected OcflStorageService $storageService, |
||||||
|
) { |
||||||
|
parent::__construct($config_factory); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function create(ContainerInterface $container): static { |
||||||
|
return new static( |
||||||
|
$container->get('config.factory'), |
||||||
|
$container->get('entity_type.manager'), |
||||||
|
$container->get('ocfl_export.storage'), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
protected function getEditableConfigNames(): array { |
||||||
|
return ['ocfl_export.settings']; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function getFormId(): string { |
||||||
|
return 'ocfl_export_settings_form'; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function buildForm(array $form, FormStateInterface $form_state): array { |
||||||
|
$config = $this->config('ocfl_export.settings'); |
||||||
|
|
||||||
|
// Storage status. |
||||||
|
$form['status'] = [ |
||||||
|
'#type' => 'details', |
||||||
|
'#title' => $this->t('Storage Status'), |
||||||
|
'#open' => TRUE, |
||||||
|
]; |
||||||
|
|
||||||
|
$initialized = $this->storageService->isInitialized(); |
||||||
|
$form['status']['storage_status'] = [ |
||||||
|
'#type' => 'item', |
||||||
|
'#title' => $this->t('OCFL Storage'), |
||||||
|
'#markup' => $initialized |
||||||
|
? $this->t('<span style="color: green;">Initialized</span> at @path', ['@path' => $this->storageService->getStorageRoot()]) |
||||||
|
: $this->t('<span style="color: orange;">Not initialized</span> - will be created on first export'), |
||||||
|
]; |
||||||
|
|
||||||
|
// Storage settings. |
||||||
|
$form['storage'] = [ |
||||||
|
'#type' => 'details', |
||||||
|
'#title' => $this->t('Storage Settings'), |
||||||
|
'#open' => TRUE, |
||||||
|
]; |
||||||
|
|
||||||
|
$form['storage']['storage_root'] = [ |
||||||
|
'#type' => 'textfield', |
||||||
|
'#title' => $this->t('Storage Root Path'), |
||||||
|
'#description' => $this->t('The filesystem path where OCFL objects will be stored. Use stream wrappers like private:// or public://, or an absolute path.'), |
||||||
|
'#default_value' => $config->get('storage_root'), |
||||||
|
'#required' => TRUE, |
||||||
|
]; |
||||||
|
|
||||||
|
$form['storage']['ocfl_version'] = [ |
||||||
|
'#type' => 'select', |
||||||
|
'#title' => $this->t('OCFL Version'), |
||||||
|
'#options' => [ |
||||||
|
'1.1' => '1.1', |
||||||
|
'1.0' => '1.0', |
||||||
|
], |
||||||
|
'#default_value' => $config->get('ocfl_version') ?: '1.1', |
||||||
|
'#description' => $this->t('The OCFL specification version to use.'), |
||||||
|
]; |
||||||
|
|
||||||
|
// Export settings. |
||||||
|
$form['export'] = [ |
||||||
|
'#type' => 'details', |
||||||
|
'#title' => $this->t('Export Settings'), |
||||||
|
'#open' => TRUE, |
||||||
|
]; |
||||||
|
|
||||||
|
$form['export']['auto_export_enabled'] = [ |
||||||
|
'#type' => 'checkbox', |
||||||
|
'#title' => $this->t('Enable automatic export on node save'), |
||||||
|
'#description' => $this->t('When enabled, nodes will be automatically exported to OCFL when saved.'), |
||||||
|
'#default_value' => $config->get('auto_export_enabled'), |
||||||
|
]; |
||||||
|
|
||||||
|
// Get all content types. |
||||||
|
$contentTypes = $this->entityTypeManager->getStorage('node_type')->loadMultiple(); |
||||||
|
$options = []; |
||||||
|
foreach ($contentTypes as $type) { |
||||||
|
$options[$type->id()] = $type->label(); |
||||||
|
} |
||||||
|
|
||||||
|
$form['export']['content_types'] = [ |
||||||
|
'#type' => 'checkboxes', |
||||||
|
'#title' => $this->t('Content Types to Export'), |
||||||
|
'#description' => $this->t('Select which content types should be exported. Leave empty to export all content types.'), |
||||||
|
'#options' => $options, |
||||||
|
'#default_value' => $config->get('content_types') ?: [], |
||||||
|
]; |
||||||
|
|
||||||
|
// Media export settings. |
||||||
|
$form['media_export'] = [ |
||||||
|
'#type' => 'details', |
||||||
|
'#title' => $this->t('Media Export Settings'), |
||||||
|
'#open' => TRUE, |
||||||
|
]; |
||||||
|
|
||||||
|
$form['media_export']['auto_export_media_enabled'] = [ |
||||||
|
'#type' => 'checkbox', |
||||||
|
'#title' => $this->t('Enable automatic export on media save'), |
||||||
|
'#description' => $this->t('When enabled, media entities will be automatically exported to OCFL when saved.'), |
||||||
|
'#default_value' => $config->get('auto_export_media_enabled'), |
||||||
|
]; |
||||||
|
|
||||||
|
// Get all media types. |
||||||
|
$mediaTypes = $this->entityTypeManager->getStorage('media_type')->loadMultiple(); |
||||||
|
$mediaOptions = []; |
||||||
|
foreach ($mediaTypes as $type) { |
||||||
|
$mediaOptions[$type->id()] = $type->label(); |
||||||
|
} |
||||||
|
|
||||||
|
$form['media_export']['media_types'] = [ |
||||||
|
'#type' => 'checkboxes', |
||||||
|
'#title' => $this->t('Media Types to Export'), |
||||||
|
'#description' => $this->t('Select which media types should be exported. Leave empty to export all media types.'), |
||||||
|
'#options' => $mediaOptions, |
||||||
|
'#default_value' => $config->get('media_types') ?: [], |
||||||
|
]; |
||||||
|
|
||||||
|
// Initialize button. |
||||||
|
$form['actions']['initialize'] = [ |
||||||
|
'#type' => 'submit', |
||||||
|
'#value' => $this->t('Initialize Storage'), |
||||||
|
'#submit' => ['::initializeStorage'], |
||||||
|
'#weight' => 10, |
||||||
|
]; |
||||||
|
|
||||||
|
return parent::buildForm($form, $form_state); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function validateForm(array &$form, FormStateInterface $form_state): void { |
||||||
|
parent::validateForm($form, $form_state); |
||||||
|
|
||||||
|
$storagePath = $form_state->getValue('storage_root'); |
||||||
|
|
||||||
|
// Check if path uses a valid stream wrapper or is an absolute path. |
||||||
|
if (!str_contains($storagePath, '://') && !str_starts_with($storagePath, '/')) { |
||||||
|
$form_state->setErrorByName('storage_root', $this->t('Storage path must be an absolute path or use a stream wrapper (e.g., private://).')); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function submitForm(array &$form, FormStateInterface $form_state): void { |
||||||
|
// Filter out unchecked content types. |
||||||
|
$contentTypes = array_filter($form_state->getValue('content_types')); |
||||||
|
// Filter out unchecked media types. |
||||||
|
$mediaTypes = array_filter($form_state->getValue('media_types')); |
||||||
|
|
||||||
|
$this->config('ocfl_export.settings') |
||||||
|
->set('storage_root', $form_state->getValue('storage_root')) |
||||||
|
->set('auto_export_enabled', (bool) $form_state->getValue('auto_export_enabled')) |
||||||
|
->set('content_types', array_values($contentTypes)) |
||||||
|
->set('auto_export_media_enabled', (bool) $form_state->getValue('auto_export_media_enabled')) |
||||||
|
->set('media_types', array_values($mediaTypes)) |
||||||
|
->set('ocfl_version', $form_state->getValue('ocfl_version')) |
||||||
|
->save(); |
||||||
|
|
||||||
|
parent::submitForm($form, $form_state); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Submit handler to initialize storage. |
||||||
|
*/ |
||||||
|
public function initializeStorage(array &$form, FormStateInterface $form_state): void { |
||||||
|
// Save config first. |
||||||
|
$this->submitForm($form, $form_state); |
||||||
|
|
||||||
|
// Initialize storage. |
||||||
|
if ($this->storageService->initializeStorageRoot()) { |
||||||
|
$this->messenger()->addStatus($this->t('OCFL storage has been initialized at @path.', [ |
||||||
|
'@path' => $this->storageService->getStorageRoot(), |
||||||
|
])); |
||||||
|
} |
||||||
|
else { |
||||||
|
$this->messenger()->addError($this->t('Failed to initialize OCFL storage. Check the logs for details.')); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,501 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Drupal\ocfl_export; |
||||||
|
|
||||||
|
use Drupal\Component\Datetime\TimeInterface; |
||||||
|
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||||
|
use Drupal\Core\Field\FieldItemListInterface; |
||||||
|
use Drupal\file\FileInterface; |
||||||
|
use Drupal\media\MediaInterface; |
||||||
|
use Drupal\node\NodeInterface; |
||||||
|
use Drupal\paragraphs\ParagraphInterface; |
||||||
|
use Drupal\taxonomy\TermInterface; |
||||||
|
use Drupal\user\UserInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Service for serializing media entities to JSON format. |
||||||
|
*/ |
||||||
|
class MediaSerializerService { |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
protected EntityTypeManagerInterface $entityTypeManager, |
||||||
|
protected TimeInterface $time, |
||||||
|
) {} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a media entity to an array suitable for JSON encoding. |
||||||
|
* |
||||||
|
* @param \Drupal\media\MediaInterface $media |
||||||
|
* The media entity to serialize. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized media data. |
||||||
|
*/ |
||||||
|
public function serialize(MediaInterface $media): array { |
||||||
|
$owner = $media->getOwner(); |
||||||
|
|
||||||
|
$data = [ |
||||||
|
'uuid' => $media->uuid(), |
||||||
|
'type' => $media->bundle(), |
||||||
|
'name' => $media->getName(), |
||||||
|
'created' => $this->formatTimestamp((int) $media->getCreatedTime()), |
||||||
|
'changed' => $this->formatTimestamp((int) $media->getChangedTime()), |
||||||
|
'status' => $media->isPublished(), |
||||||
|
'owner' => [ |
||||||
|
'uid' => (int) $owner->id(), |
||||||
|
'name' => $owner->getAccountName(), |
||||||
|
'display_name' => $owner->getDisplayName(), |
||||||
|
], |
||||||
|
'source' => $this->serializeMediaSource($media), |
||||||
|
'fields' => $this->serializeFields($media), |
||||||
|
'_metadata' => [ |
||||||
|
'ocfl_version' => '1.1', |
||||||
|
'export_timestamp' => $this->formatTimestamp($this->time->getRequestTime()), |
||||||
|
'drupal_version' => \Drupal::VERSION, |
||||||
|
'entity_type' => 'media', |
||||||
|
], |
||||||
|
]; |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize the media source (primary file/data). |
||||||
|
* |
||||||
|
* @param \Drupal\media\MediaInterface $media |
||||||
|
* The media entity. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized source data. |
||||||
|
*/ |
||||||
|
protected function serializeMediaSource(MediaInterface $media): array { |
||||||
|
$source = $media->getSource(); |
||||||
|
$sourceConfig = $source->getConfiguration(); |
||||||
|
$sourceField = $sourceConfig['source_field'] ?? NULL; |
||||||
|
|
||||||
|
$data = [ |
||||||
|
'plugin_id' => $source->getPluginId(), |
||||||
|
]; |
||||||
|
|
||||||
|
if ($sourceField && $media->hasField($sourceField)) { |
||||||
|
$sourceFieldItems = $media->get($sourceField); |
||||||
|
if (!$sourceFieldItems->isEmpty()) { |
||||||
|
$sourceItem = $sourceFieldItems->first(); |
||||||
|
$file = $sourceItem->entity ?? NULL; |
||||||
|
|
||||||
|
if ($file instanceof FileInterface) { |
||||||
|
$data['file'] = [ |
||||||
|
'uuid' => $file->uuid(), |
||||||
|
'filename' => $file->getFilename(), |
||||||
|
'uri' => $file->getFileUri(), |
||||||
|
'filemime' => $file->getMimeType(), |
||||||
|
'filesize' => (int) $file->getSize(), |
||||||
|
'url' => \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri()), |
||||||
|
]; |
||||||
|
|
||||||
|
// Include alt and title if available (for images). |
||||||
|
if (isset($sourceItem->alt)) { |
||||||
|
$data['alt'] = $sourceItem->alt; |
||||||
|
} |
||||||
|
if (isset($sourceItem->title)) { |
||||||
|
$data['title'] = $sourceItem->title; |
||||||
|
} |
||||||
|
if (isset($sourceItem->width)) { |
||||||
|
$data['width'] = (int) $sourceItem->width; |
||||||
|
} |
||||||
|
if (isset($sourceItem->height)) { |
||||||
|
$data['height'] = (int) $sourceItem->height; |
||||||
|
} |
||||||
|
} |
||||||
|
else { |
||||||
|
// For non-file sources (e.g., oEmbed, remote video). |
||||||
|
$data['value'] = $sourceItem->getValue(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize all fields of a media entity. |
||||||
|
* |
||||||
|
* @param \Drupal\media\MediaInterface $media |
||||||
|
* The media entity to serialize fields from. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized field data. |
||||||
|
*/ |
||||||
|
protected function serializeFields(MediaInterface $media): array { |
||||||
|
$fields = []; |
||||||
|
$fieldDefinitions = $media->getFieldDefinitions(); |
||||||
|
$sourceField = $media->getSource()->getConfiguration()['source_field'] ?? NULL; |
||||||
|
|
||||||
|
foreach ($fieldDefinitions as $fieldName => $definition) { |
||||||
|
// Skip base fields and source field (already serialized). |
||||||
|
if ($this->isBaseField($fieldName) || $fieldName === $sourceField) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$fieldItems = $media->get($fieldName); |
||||||
|
if ($fieldItems->isEmpty()) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$fields[$fieldName] = $this->serializeFieldItems($fieldItems); |
||||||
|
} |
||||||
|
|
||||||
|
return $fields; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Check if a field is a base field that should be skipped. |
||||||
|
* |
||||||
|
* @param string $fieldName |
||||||
|
* The field name. |
||||||
|
* |
||||||
|
* @return bool |
||||||
|
* TRUE if this is a base field to skip. |
||||||
|
*/ |
||||||
|
protected function isBaseField(string $fieldName): bool { |
||||||
|
$baseFields = [ |
||||||
|
'mid', |
||||||
|
'vid', |
||||||
|
'uuid', |
||||||
|
'langcode', |
||||||
|
'bundle', |
||||||
|
'revision_created', |
||||||
|
'revision_user', |
||||||
|
'revision_log_message', |
||||||
|
'status', |
||||||
|
'uid', |
||||||
|
'name', |
||||||
|
'thumbnail', |
||||||
|
'created', |
||||||
|
'changed', |
||||||
|
'default_langcode', |
||||||
|
'revision_default', |
||||||
|
'revision_translation_affected', |
||||||
|
]; |
||||||
|
|
||||||
|
return in_array($fieldName, $baseFields, TRUE); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize field items. |
||||||
|
* |
||||||
|
* @param \Drupal\Core\Field\FieldItemListInterface $fieldItems |
||||||
|
* The field items to serialize. |
||||||
|
* |
||||||
|
* @return mixed |
||||||
|
* The serialized field data. |
||||||
|
*/ |
||||||
|
protected function serializeFieldItems(FieldItemListInterface $fieldItems): mixed { |
||||||
|
$fieldType = $fieldItems->getFieldDefinition()->getType(); |
||||||
|
$values = []; |
||||||
|
|
||||||
|
foreach ($fieldItems as $delta => $item) { |
||||||
|
$value = $this->serializeFieldItem($item, $fieldType); |
||||||
|
if ($value !== NULL) { |
||||||
|
$values[] = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Return single value for single-value fields. |
||||||
|
if (count($values) === 1 && $fieldItems->getFieldDefinition()->getFieldStorageDefinition()->getCardinality() === 1) { |
||||||
|
return $values[0]; |
||||||
|
} |
||||||
|
|
||||||
|
return $values; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a single field item. |
||||||
|
* |
||||||
|
* @param mixed $item |
||||||
|
* The field item. |
||||||
|
* @param string $fieldType |
||||||
|
* The field type. |
||||||
|
* |
||||||
|
* @return mixed |
||||||
|
* The serialized value. |
||||||
|
*/ |
||||||
|
protected function serializeFieldItem(mixed $item, string $fieldType): mixed { |
||||||
|
return match ($fieldType) { |
||||||
|
'entity_reference' => $this->serializeEntityReference($item), |
||||||
|
'entity_reference_revisions' => $this->serializeEntityReferenceRevisions($item), |
||||||
|
'text_with_summary' => [ |
||||||
|
'value' => $item->value, |
||||||
|
'format' => $item->format, |
||||||
|
'summary' => $item->summary ?? '', |
||||||
|
], |
||||||
|
'text_long', 'text' => [ |
||||||
|
'value' => $item->value, |
||||||
|
'format' => $item->format ?? NULL, |
||||||
|
], |
||||||
|
'image' => [ |
||||||
|
'target_id' => (int) $item->target_id, |
||||||
|
'alt' => $item->alt ?? '', |
||||||
|
'title' => $item->title ?? '', |
||||||
|
'width' => $item->width ? (int) $item->width : NULL, |
||||||
|
'height' => $item->height ? (int) $item->height : NULL, |
||||||
|
'entity' => $this->serializeFileEntity($item->entity), |
||||||
|
], |
||||||
|
'file' => [ |
||||||
|
'target_id' => (int) $item->target_id, |
||||||
|
'description' => $item->description ?? '', |
||||||
|
'display' => $item->display ?? TRUE, |
||||||
|
'entity' => $this->serializeFileEntity($item->entity), |
||||||
|
], |
||||||
|
'link' => [ |
||||||
|
'uri' => $item->uri, |
||||||
|
'title' => $item->title ?? '', |
||||||
|
'options' => $item->options ?? [], |
||||||
|
], |
||||||
|
'datetime' => $item->value, |
||||||
|
'daterange' => [ |
||||||
|
'value' => $item->value, |
||||||
|
'end_value' => $item->end_value, |
||||||
|
], |
||||||
|
'boolean' => (bool) $item->value, |
||||||
|
'integer', 'list_integer' => (int) $item->value, |
||||||
|
'decimal', 'float', 'list_float' => (float) $item->value, |
||||||
|
'email' => $item->value, |
||||||
|
'telephone' => $item->value, |
||||||
|
default => $item->getValue(), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize an entity reference field item. |
||||||
|
* |
||||||
|
* @param mixed $item |
||||||
|
* The field item. |
||||||
|
* |
||||||
|
* @return array|null |
||||||
|
* The serialized reference or NULL. |
||||||
|
*/ |
||||||
|
protected function serializeEntityReference(mixed $item): ?array { |
||||||
|
$entity = $item->entity; |
||||||
|
if (!$entity) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
$data = [ |
||||||
|
'target_id' => (int) $item->target_id, |
||||||
|
]; |
||||||
|
|
||||||
|
// Dereference based on entity type. |
||||||
|
$data['entity'] = match ($entity->getEntityTypeId()) { |
||||||
|
'taxonomy_term' => $this->serializeTaxonomyTerm($entity), |
||||||
|
'media' => $this->serializeMediaReference($entity), |
||||||
|
'user' => $this->serializeUser($entity), |
||||||
|
'node' => $this->serializeNodeReference($entity), |
||||||
|
'file' => $this->serializeFileEntity($entity), |
||||||
|
default => [ |
||||||
|
'uuid' => $entity->uuid(), |
||||||
|
'type' => $entity->bundle(), |
||||||
|
'label' => $entity->label(), |
||||||
|
], |
||||||
|
}; |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize an entity reference revisions field item (paragraphs). |
||||||
|
* |
||||||
|
* @param mixed $item |
||||||
|
* The field item. |
||||||
|
* |
||||||
|
* @return array|null |
||||||
|
* The serialized reference or NULL. |
||||||
|
*/ |
||||||
|
protected function serializeEntityReferenceRevisions(mixed $item): ?array { |
||||||
|
$entity = $item->entity; |
||||||
|
if (!$entity) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
if ($entity instanceof ParagraphInterface) { |
||||||
|
return $this->serializeParagraph($entity); |
||||||
|
} |
||||||
|
|
||||||
|
return [ |
||||||
|
'target_id' => (int) $item->target_id, |
||||||
|
'target_revision_id' => (int) $item->target_revision_id, |
||||||
|
'entity' => [ |
||||||
|
'uuid' => $entity->uuid(), |
||||||
|
'type' => $entity->bundle(), |
||||||
|
], |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a taxonomy term. |
||||||
|
* |
||||||
|
* @param \Drupal\taxonomy\TermInterface $term |
||||||
|
* The taxonomy term. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized term data. |
||||||
|
*/ |
||||||
|
protected function serializeTaxonomyTerm(TermInterface $term): array { |
||||||
|
return [ |
||||||
|
'uuid' => $term->uuid(), |
||||||
|
'name' => $term->getName(), |
||||||
|
'description' => $term->getDescription(), |
||||||
|
'vocabulary' => $term->bundle(), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a media reference (1 level only - no recursion). |
||||||
|
* |
||||||
|
* @param \Drupal\media\MediaInterface $media |
||||||
|
* The referenced media entity. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized media reference data. |
||||||
|
*/ |
||||||
|
protected function serializeMediaReference(MediaInterface $media): array { |
||||||
|
$data = [ |
||||||
|
'uuid' => $media->uuid(), |
||||||
|
'bundle' => $media->bundle(), |
||||||
|
'name' => $media->getName(), |
||||||
|
'status' => $media->isPublished(), |
||||||
|
'created' => $this->formatTimestamp((int) $media->getCreatedTime()), |
||||||
|
'changed' => $this->formatTimestamp((int) $media->getChangedTime()), |
||||||
|
]; |
||||||
|
|
||||||
|
// Get the source field value. |
||||||
|
$sourceField = $media->getSource()->getConfiguration()['source_field'] ?? NULL; |
||||||
|
if ($sourceField && $media->hasField($sourceField)) { |
||||||
|
$sourceValue = $media->get($sourceField)->first(); |
||||||
|
if ($sourceValue) { |
||||||
|
$file = $sourceValue->entity ?? NULL; |
||||||
|
if ($file instanceof FileInterface) { |
||||||
|
$data['uri'] = $file->getFileUri(); |
||||||
|
$data['url'] = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri()); |
||||||
|
} |
||||||
|
if (isset($sourceValue->alt)) { |
||||||
|
$data['alt'] = $sourceValue->alt; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a user entity. |
||||||
|
* |
||||||
|
* @param \Drupal\user\UserInterface $user |
||||||
|
* The user entity. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized user data. |
||||||
|
*/ |
||||||
|
protected function serializeUser(UserInterface $user): array { |
||||||
|
return [ |
||||||
|
'uuid' => $user->uuid(), |
||||||
|
'uid' => (int) $user->id(), |
||||||
|
'name' => $user->getAccountName(), |
||||||
|
'display_name' => $user->getDisplayName(), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a node reference (1 level only). |
||||||
|
* |
||||||
|
* @param \Drupal\node\NodeInterface $node |
||||||
|
* The referenced node. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized node reference data. |
||||||
|
*/ |
||||||
|
protected function serializeNodeReference(NodeInterface $node): array { |
||||||
|
return [ |
||||||
|
'uuid' => $node->uuid(), |
||||||
|
'type' => $node->bundle(), |
||||||
|
'title' => $node->getTitle(), |
||||||
|
'status' => $node->isPublished(), |
||||||
|
'created' => $this->formatTimestamp((int) $node->getCreatedTime()), |
||||||
|
'changed' => $this->formatTimestamp((int) $node->getChangedTime()), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a paragraph entity. |
||||||
|
* |
||||||
|
* @param \Drupal\paragraphs\ParagraphInterface $paragraph |
||||||
|
* The paragraph entity. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized paragraph data. |
||||||
|
*/ |
||||||
|
protected function serializeParagraph(ParagraphInterface $paragraph): array { |
||||||
|
$data = [ |
||||||
|
'uuid' => $paragraph->uuid(), |
||||||
|
'type' => $paragraph->bundle(), |
||||||
|
'fields' => [], |
||||||
|
]; |
||||||
|
|
||||||
|
$fieldDefinitions = $paragraph->getFieldDefinitions(); |
||||||
|
|
||||||
|
foreach ($fieldDefinitions as $fieldName => $definition) { |
||||||
|
// Skip base paragraph fields. |
||||||
|
if (in_array($fieldName, ['id', 'uuid', 'revision_id', 'langcode', 'type', 'status', 'created', 'parent_id', 'parent_type', 'parent_field_name', 'behavior_settings', 'default_langcode', 'revision_default', 'revision_translation_affected'], TRUE)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$fieldItems = $paragraph->get($fieldName); |
||||||
|
if ($fieldItems->isEmpty()) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$data['fields'][$fieldName] = $this->serializeFieldItems($fieldItems); |
||||||
|
} |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a file entity. |
||||||
|
* |
||||||
|
* @param mixed $file |
||||||
|
* The file entity. |
||||||
|
* |
||||||
|
* @return array|null |
||||||
|
* The serialized file data or NULL. |
||||||
|
*/ |
||||||
|
protected function serializeFileEntity(mixed $file): ?array { |
||||||
|
if (!$file) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
return [ |
||||||
|
'uuid' => $file->uuid(), |
||||||
|
'filename' => $file->getFilename(), |
||||||
|
'uri' => $file->getFileUri(), |
||||||
|
'filemime' => $file->getMimeType(), |
||||||
|
'filesize' => (int) $file->getSize(), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Format a timestamp as ISO 8601. |
||||||
|
* |
||||||
|
* @param int $timestamp |
||||||
|
* The Unix timestamp. |
||||||
|
* |
||||||
|
* @return string |
||||||
|
* The formatted date string. |
||||||
|
*/ |
||||||
|
protected function formatTimestamp(int $timestamp): string { |
||||||
|
return date('c', $timestamp); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,437 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Drupal\ocfl_export; |
||||||
|
|
||||||
|
use Drupal\Component\Datetime\TimeInterface; |
||||||
|
use Drupal\Core\Entity\EntityTypeManagerInterface; |
||||||
|
use Drupal\Core\Field\FieldItemListInterface; |
||||||
|
use Drupal\media\MediaInterface; |
||||||
|
use Drupal\node\NodeInterface; |
||||||
|
use Drupal\paragraphs\ParagraphInterface; |
||||||
|
use Drupal\taxonomy\TermInterface; |
||||||
|
use Drupal\user\UserInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Service for serializing nodes to JSON format. |
||||||
|
*/ |
||||||
|
class NodeSerializerService { |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
protected EntityTypeManagerInterface $entityTypeManager, |
||||||
|
protected TimeInterface $time, |
||||||
|
) {} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a node to an array suitable for JSON encoding. |
||||||
|
* |
||||||
|
* @param \Drupal\node\NodeInterface $node |
||||||
|
* The node to serialize. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized node data. |
||||||
|
*/ |
||||||
|
public function serialize(NodeInterface $node): array { |
||||||
|
$author = $node->getOwner(); |
||||||
|
|
||||||
|
$data = [ |
||||||
|
'uuid' => $node->uuid(), |
||||||
|
'type' => $node->bundle(), |
||||||
|
'title' => $node->getTitle(), |
||||||
|
'created' => $this->formatTimestamp((int) $node->getCreatedTime()), |
||||||
|
'changed' => $this->formatTimestamp((int) $node->getChangedTime()), |
||||||
|
'status' => $node->isPublished(), |
||||||
|
'author' => [ |
||||||
|
'uid' => (int) $author->id(), |
||||||
|
'name' => $author->getAccountName(), |
||||||
|
'display_name' => $author->getDisplayName(), |
||||||
|
], |
||||||
|
'fields' => $this->serializeFields($node), |
||||||
|
'_metadata' => [ |
||||||
|
'ocfl_version' => '1.1', |
||||||
|
'export_timestamp' => $this->formatTimestamp($this->time->getRequestTime()), |
||||||
|
'drupal_version' => \Drupal::VERSION, |
||||||
|
], |
||||||
|
]; |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize all fields of a node. |
||||||
|
* |
||||||
|
* @param \Drupal\node\NodeInterface $node |
||||||
|
* The node to serialize fields from. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized field data. |
||||||
|
*/ |
||||||
|
protected function serializeFields(NodeInterface $node): array { |
||||||
|
$fields = []; |
||||||
|
$fieldDefinitions = $node->getFieldDefinitions(); |
||||||
|
|
||||||
|
foreach ($fieldDefinitions as $fieldName => $definition) { |
||||||
|
// Skip base fields that are already included in the main structure. |
||||||
|
if ($this->isBaseField($fieldName)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$fieldItems = $node->get($fieldName); |
||||||
|
if ($fieldItems->isEmpty()) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$fields[$fieldName] = $this->serializeFieldItems($fieldItems); |
||||||
|
} |
||||||
|
|
||||||
|
return $fields; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Check if a field is a base field that should be skipped. |
||||||
|
* |
||||||
|
* @param string $fieldName |
||||||
|
* The field name. |
||||||
|
* |
||||||
|
* @return bool |
||||||
|
* TRUE if this is a base field to skip. |
||||||
|
*/ |
||||||
|
protected function isBaseField(string $fieldName): bool { |
||||||
|
$baseFields = [ |
||||||
|
'nid', |
||||||
|
'vid', |
||||||
|
'uuid', |
||||||
|
'langcode', |
||||||
|
'type', |
||||||
|
'revision_timestamp', |
||||||
|
'revision_uid', |
||||||
|
'revision_log', |
||||||
|
'status', |
||||||
|
'uid', |
||||||
|
'title', |
||||||
|
'created', |
||||||
|
'changed', |
||||||
|
'promote', |
||||||
|
'sticky', |
||||||
|
'default_langcode', |
||||||
|
'revision_default', |
||||||
|
'revision_translation_affected', |
||||||
|
]; |
||||||
|
|
||||||
|
return in_array($fieldName, $baseFields, TRUE); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize field items. |
||||||
|
* |
||||||
|
* @param \Drupal\Core\Field\FieldItemListInterface $fieldItems |
||||||
|
* The field items to serialize. |
||||||
|
* |
||||||
|
* @return mixed |
||||||
|
* The serialized field data. |
||||||
|
*/ |
||||||
|
protected function serializeFieldItems(FieldItemListInterface $fieldItems): mixed { |
||||||
|
$fieldType = $fieldItems->getFieldDefinition()->getType(); |
||||||
|
$values = []; |
||||||
|
|
||||||
|
foreach ($fieldItems as $delta => $item) { |
||||||
|
$value = $this->serializeFieldItem($item, $fieldType); |
||||||
|
if ($value !== NULL) { |
||||||
|
$values[] = $value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Return single value for single-value fields. |
||||||
|
if (count($values) === 1 && $fieldItems->getFieldDefinition()->getFieldStorageDefinition()->getCardinality() === 1) { |
||||||
|
return $values[0]; |
||||||
|
} |
||||||
|
|
||||||
|
return $values; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a single field item. |
||||||
|
* |
||||||
|
* @param mixed $item |
||||||
|
* The field item. |
||||||
|
* @param string $fieldType |
||||||
|
* The field type. |
||||||
|
* |
||||||
|
* @return mixed |
||||||
|
* The serialized value. |
||||||
|
*/ |
||||||
|
protected function serializeFieldItem(mixed $item, string $fieldType): mixed { |
||||||
|
return match ($fieldType) { |
||||||
|
'entity_reference' => $this->serializeEntityReference($item), |
||||||
|
'entity_reference_revisions' => $this->serializeEntityReferenceRevisions($item), |
||||||
|
'text_with_summary' => [ |
||||||
|
'value' => $item->value, |
||||||
|
'format' => $item->format, |
||||||
|
'summary' => $item->summary ?? '', |
||||||
|
], |
||||||
|
'text_long', 'text' => [ |
||||||
|
'value' => $item->value, |
||||||
|
'format' => $item->format ?? NULL, |
||||||
|
], |
||||||
|
'image' => [ |
||||||
|
'target_id' => (int) $item->target_id, |
||||||
|
'alt' => $item->alt ?? '', |
||||||
|
'title' => $item->title ?? '', |
||||||
|
'width' => $item->width ? (int) $item->width : NULL, |
||||||
|
'height' => $item->height ? (int) $item->height : NULL, |
||||||
|
'entity' => $this->serializeFileEntity($item->entity), |
||||||
|
], |
||||||
|
'file' => [ |
||||||
|
'target_id' => (int) $item->target_id, |
||||||
|
'description' => $item->description ?? '', |
||||||
|
'display' => $item->display ?? TRUE, |
||||||
|
'entity' => $this->serializeFileEntity($item->entity), |
||||||
|
], |
||||||
|
'link' => [ |
||||||
|
'uri' => $item->uri, |
||||||
|
'title' => $item->title ?? '', |
||||||
|
'options' => $item->options ?? [], |
||||||
|
], |
||||||
|
'datetime' => $item->value, |
||||||
|
'daterange' => [ |
||||||
|
'value' => $item->value, |
||||||
|
'end_value' => $item->end_value, |
||||||
|
], |
||||||
|
'boolean' => (bool) $item->value, |
||||||
|
'integer', 'list_integer' => (int) $item->value, |
||||||
|
'decimal', 'float', 'list_float' => (float) $item->value, |
||||||
|
'email' => $item->value, |
||||||
|
'telephone' => $item->value, |
||||||
|
default => $item->getValue(), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize an entity reference field item. |
||||||
|
* |
||||||
|
* @param mixed $item |
||||||
|
* The field item. |
||||||
|
* |
||||||
|
* @return array|null |
||||||
|
* The serialized reference or NULL. |
||||||
|
*/ |
||||||
|
protected function serializeEntityReference(mixed $item): ?array { |
||||||
|
$entity = $item->entity; |
||||||
|
if (!$entity) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
$data = [ |
||||||
|
'target_id' => (int) $item->target_id, |
||||||
|
]; |
||||||
|
|
||||||
|
// Dereference based on entity type. |
||||||
|
$data['entity'] = match ($entity->getEntityTypeId()) { |
||||||
|
'taxonomy_term' => $this->serializeTaxonomyTerm($entity), |
||||||
|
'media' => $this->serializeMedia($entity), |
||||||
|
'user' => $this->serializeUser($entity), |
||||||
|
'node' => $this->serializeNodeReference($entity), |
||||||
|
default => [ |
||||||
|
'uuid' => $entity->uuid(), |
||||||
|
'type' => $entity->bundle(), |
||||||
|
'label' => $entity->label(), |
||||||
|
], |
||||||
|
}; |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize an entity reference revisions field item (paragraphs). |
||||||
|
* |
||||||
|
* @param mixed $item |
||||||
|
* The field item. |
||||||
|
* |
||||||
|
* @return array|null |
||||||
|
* The serialized reference or NULL. |
||||||
|
*/ |
||||||
|
protected function serializeEntityReferenceRevisions(mixed $item): ?array { |
||||||
|
$entity = $item->entity; |
||||||
|
if (!$entity) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
if ($entity instanceof ParagraphInterface) { |
||||||
|
return $this->serializeParagraph($entity); |
||||||
|
} |
||||||
|
|
||||||
|
return [ |
||||||
|
'target_id' => (int) $item->target_id, |
||||||
|
'target_revision_id' => (int) $item->target_revision_id, |
||||||
|
'entity' => [ |
||||||
|
'uuid' => $entity->uuid(), |
||||||
|
'type' => $entity->bundle(), |
||||||
|
], |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a taxonomy term. |
||||||
|
* |
||||||
|
* @param \Drupal\taxonomy\TermInterface $term |
||||||
|
* The taxonomy term. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized term data. |
||||||
|
*/ |
||||||
|
protected function serializeTaxonomyTerm(TermInterface $term): array { |
||||||
|
return [ |
||||||
|
'uuid' => $term->uuid(), |
||||||
|
'name' => $term->getName(), |
||||||
|
'description' => $term->getDescription(), |
||||||
|
'vocabulary' => $term->bundle(), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a media entity. |
||||||
|
* |
||||||
|
* @param \Drupal\media\MediaInterface $media |
||||||
|
* The media entity. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized media data. |
||||||
|
*/ |
||||||
|
protected function serializeMedia(MediaInterface $media): array { |
||||||
|
$data = [ |
||||||
|
'uuid' => $media->uuid(), |
||||||
|
'name' => $media->getName(), |
||||||
|
'bundle' => $media->bundle(), |
||||||
|
]; |
||||||
|
|
||||||
|
// Get the source field value. |
||||||
|
$sourceField = $media->getSource()->getConfiguration()['source_field'] ?? NULL; |
||||||
|
if ($sourceField && $media->hasField($sourceField)) { |
||||||
|
$sourceValue = $media->get($sourceField)->first(); |
||||||
|
if ($sourceValue) { |
||||||
|
$file = $sourceValue->entity ?? NULL; |
||||||
|
if ($file) { |
||||||
|
$data['uri'] = $file->getFileUri(); |
||||||
|
$data['url'] = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri()); |
||||||
|
} |
||||||
|
// Include alt text if available. |
||||||
|
if (isset($sourceValue->alt)) { |
||||||
|
$data['alt'] = $sourceValue->alt; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a user entity. |
||||||
|
* |
||||||
|
* @param \Drupal\user\UserInterface $user |
||||||
|
* The user entity. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized user data. |
||||||
|
*/ |
||||||
|
protected function serializeUser(UserInterface $user): array { |
||||||
|
return [ |
||||||
|
'uuid' => $user->uuid(), |
||||||
|
'uid' => (int) $user->id(), |
||||||
|
'name' => $user->getAccountName(), |
||||||
|
'display_name' => $user->getDisplayName(), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a node reference (1 level only). |
||||||
|
* |
||||||
|
* @param \Drupal\node\NodeInterface $node |
||||||
|
* The referenced node. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized node reference data. |
||||||
|
*/ |
||||||
|
protected function serializeNodeReference(NodeInterface $node): array { |
||||||
|
return [ |
||||||
|
'uuid' => $node->uuid(), |
||||||
|
'type' => $node->bundle(), |
||||||
|
'title' => $node->getTitle(), |
||||||
|
'status' => $node->isPublished(), |
||||||
|
'created' => $this->formatTimestamp((int) $node->getCreatedTime()), |
||||||
|
'changed' => $this->formatTimestamp((int) $node->getChangedTime()), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a paragraph entity. |
||||||
|
* |
||||||
|
* @param \Drupal\paragraphs\ParagraphInterface $paragraph |
||||||
|
* The paragraph entity. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The serialized paragraph data. |
||||||
|
*/ |
||||||
|
protected function serializeParagraph(ParagraphInterface $paragraph): array { |
||||||
|
$data = [ |
||||||
|
'uuid' => $paragraph->uuid(), |
||||||
|
'type' => $paragraph->bundle(), |
||||||
|
'fields' => [], |
||||||
|
]; |
||||||
|
|
||||||
|
$fieldDefinitions = $paragraph->getFieldDefinitions(); |
||||||
|
|
||||||
|
foreach ($fieldDefinitions as $fieldName => $definition) { |
||||||
|
// Skip base paragraph fields. |
||||||
|
if (in_array($fieldName, ['id', 'uuid', 'revision_id', 'langcode', 'type', 'status', 'created', 'parent_id', 'parent_type', 'parent_field_name', 'behavior_settings', 'default_langcode', 'revision_default', 'revision_translation_affected'], TRUE)) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$fieldItems = $paragraph->get($fieldName); |
||||||
|
if ($fieldItems->isEmpty()) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$data['fields'][$fieldName] = $this->serializeFieldItems($fieldItems); |
||||||
|
} |
||||||
|
|
||||||
|
return $data; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Serialize a file entity. |
||||||
|
* |
||||||
|
* @param mixed $file |
||||||
|
* The file entity. |
||||||
|
* |
||||||
|
* @return array|null |
||||||
|
* The serialized file data or NULL. |
||||||
|
*/ |
||||||
|
protected function serializeFileEntity(mixed $file): ?array { |
||||||
|
if (!$file) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
return [ |
||||||
|
'uuid' => $file->uuid(), |
||||||
|
'filename' => $file->getFilename(), |
||||||
|
'uri' => $file->getFileUri(), |
||||||
|
'filemime' => $file->getMimeType(), |
||||||
|
'filesize' => (int) $file->getSize(), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Format a timestamp as ISO 8601. |
||||||
|
* |
||||||
|
* @param int $timestamp |
||||||
|
* The Unix timestamp. |
||||||
|
* |
||||||
|
* @return string |
||||||
|
* The formatted date string. |
||||||
|
*/ |
||||||
|
protected function formatTimestamp(int $timestamp): string { |
||||||
|
return date('c', $timestamp); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,427 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Drupal\ocfl_export; |
||||||
|
|
||||||
|
use Drupal\Core\Config\ConfigFactoryInterface; |
||||||
|
use Drupal\Core\File\FileSystemInterface; |
||||||
|
use Drupal\Core\Logger\LoggerChannelFactoryInterface; |
||||||
|
use Psr\Log\LoggerInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Service for managing OCFL storage operations. |
||||||
|
*/ |
||||||
|
class OcflStorageService { |
||||||
|
|
||||||
|
protected LoggerInterface $logger; |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
protected ConfigFactoryInterface $configFactory, |
||||||
|
protected FileSystemInterface $fileSystem, |
||||||
|
LoggerChannelFactoryInterface $loggerFactory, |
||||||
|
) { |
||||||
|
$this->logger = $loggerFactory->get('ocfl_export'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the storage root path. |
||||||
|
* |
||||||
|
* @return string |
||||||
|
* The absolute path to the storage root. |
||||||
|
*/ |
||||||
|
public function getStorageRoot(): string { |
||||||
|
$config = $this->configFactory->get('ocfl_export.settings'); |
||||||
|
$path = $config->get('storage_root') ?: 'private://ocfl-storage'; |
||||||
|
|
||||||
|
return $this->fileSystem->realpath($path) ?: $path; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Initialize the OCFL storage root. |
||||||
|
* |
||||||
|
* @return bool |
||||||
|
* TRUE if initialization was successful. |
||||||
|
*/ |
||||||
|
public function initializeStorageRoot(): bool { |
||||||
|
$root = $this->getStorageRoot(); |
||||||
|
|
||||||
|
// Create directory if it doesn't exist. |
||||||
|
if (!is_dir($root)) { |
||||||
|
if (!$this->fileSystem->mkdir($root, 0755, TRUE)) { |
||||||
|
$this->logger->error('Failed to create OCFL storage root: @path', ['@path' => $root]); |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Create OCFL root declaration file. |
||||||
|
$declarationFile = $root . '/0=ocfl_1.1'; |
||||||
|
if (!file_exists($declarationFile)) { |
||||||
|
$content = "ocfl_1.1\nhttps://ocfl.io/1.1/spec/#root-conformance-declaration\n"; |
||||||
|
if (file_put_contents($declarationFile, $content) === FALSE) { |
||||||
|
$this->logger->error('Failed to create OCFL root declaration file.'); |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Create OCFL layout file. |
||||||
|
$layoutFile = $root . '/ocfl_layout.json'; |
||||||
|
if (!file_exists($layoutFile)) { |
||||||
|
$layout = [ |
||||||
|
'extension' => '0002-flat-direct-storage-layout', |
||||||
|
'description' => 'OCFL storage using flat direct layout with UUIDs.', |
||||||
|
]; |
||||||
|
if (file_put_contents($layoutFile, json_encode($layout, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)) === FALSE) { |
||||||
|
$this->logger->error('Failed to create OCFL layout file.'); |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return TRUE; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Save or update an OCFL object. |
||||||
|
* |
||||||
|
* @param string $objectId |
||||||
|
* The object ID (UUID). |
||||||
|
* @param array $data |
||||||
|
* The data to save. |
||||||
|
* |
||||||
|
* @return bool |
||||||
|
* TRUE if save was successful. |
||||||
|
*/ |
||||||
|
public function saveObject(string $objectId, array $data): bool { |
||||||
|
if (!$this->initializeStorageRoot()) { |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
|
||||||
|
$root = $this->getStorageRoot(); |
||||||
|
$objectPath = $root . '/' . $objectId; |
||||||
|
|
||||||
|
// Check if object exists and get current version. |
||||||
|
$currentVersion = $this->getCurrentVersion($objectPath); |
||||||
|
$newVersion = $currentVersion + 1; |
||||||
|
$versionDir = 'v' . $newVersion; |
||||||
|
|
||||||
|
// Create object directory structure. |
||||||
|
$contentPath = $objectPath . '/' . $versionDir . '/content'; |
||||||
|
if (!is_dir($contentPath)) { |
||||||
|
if (!$this->fileSystem->mkdir($contentPath, 0755, TRUE)) { |
||||||
|
$this->logger->error('Failed to create OCFL object content directory: @path', ['@path' => $contentPath]); |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Create object declaration file for new objects. |
||||||
|
if ($currentVersion === 0) { |
||||||
|
$declarationFile = $objectPath . '/0=ocfl_object_1.1'; |
||||||
|
$content = "ocfl_object_1.1\nhttps://ocfl.io/1.1/spec/#object-conformance-declaration\n"; |
||||||
|
if (file_put_contents($declarationFile, $content) === FALSE) { |
||||||
|
$this->logger->error('Failed to create OCFL object declaration file.'); |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Write node.json to content directory. |
||||||
|
$nodeJsonPath = $contentPath . '/node.json'; |
||||||
|
$jsonContent = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); |
||||||
|
if (file_put_contents($nodeJsonPath, $jsonContent) === FALSE) { |
||||||
|
$this->logger->error('Failed to write node.json to OCFL object.'); |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
|
||||||
|
// Generate and write inventory. |
||||||
|
$inventory = $this->generateInventory($objectId, $objectPath, $newVersion, $nodeJsonPath); |
||||||
|
|
||||||
|
// Write inventory to version directory. |
||||||
|
$versionInventoryPath = $objectPath . '/' . $versionDir . '/inventory.json'; |
||||||
|
$inventoryJson = json_encode($inventory, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); |
||||||
|
if (file_put_contents($versionInventoryPath, $inventoryJson) === FALSE) { |
||||||
|
$this->logger->error('Failed to write version inventory.json.'); |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
|
||||||
|
// Write inventory checksum to version directory. |
||||||
|
$versionChecksumPath = $objectPath . '/' . $versionDir . '/inventory.json.sha512'; |
||||||
|
$checksum = hash('sha512', $inventoryJson); |
||||||
|
if (file_put_contents($versionChecksumPath, $checksum . ' inventory.json') === FALSE) { |
||||||
|
$this->logger->error('Failed to write version inventory checksum.'); |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
|
||||||
|
// Write inventory to object root. |
||||||
|
$rootInventoryPath = $objectPath . '/inventory.json'; |
||||||
|
if (file_put_contents($rootInventoryPath, $inventoryJson) === FALSE) { |
||||||
|
$this->logger->error('Failed to write root inventory.json.'); |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
|
||||||
|
// Write inventory checksum to object root. |
||||||
|
$rootChecksumPath = $objectPath . '/inventory.json.sha512'; |
||||||
|
if (file_put_contents($rootChecksumPath, $checksum . ' inventory.json') === FALSE) { |
||||||
|
$this->logger->error('Failed to write root inventory checksum.'); |
||||||
|
return FALSE; |
||||||
|
} |
||||||
|
|
||||||
|
$this->logger->info('OCFL object @id saved as version @version.', [ |
||||||
|
'@id' => $objectId, |
||||||
|
'@version' => $versionDir, |
||||||
|
]); |
||||||
|
|
||||||
|
return TRUE; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Mark an OCFL object as deleted (for nodes). |
||||||
|
* |
||||||
|
* @param \Drupal\node\NodeInterface $node |
||||||
|
* The node being deleted. |
||||||
|
* |
||||||
|
* @return bool |
||||||
|
* TRUE if marking was successful. |
||||||
|
*/ |
||||||
|
public function markDeleted(\Drupal\node\NodeInterface $node): bool { |
||||||
|
$root = $this->getStorageRoot(); |
||||||
|
$objectPath = $root . '/' . $node->uuid(); |
||||||
|
|
||||||
|
if (!is_dir($objectPath)) { |
||||||
|
return TRUE; |
||||||
|
} |
||||||
|
|
||||||
|
// Create a new version with deletion marker. |
||||||
|
$data = [ |
||||||
|
'uuid' => $node->uuid(), |
||||||
|
'type' => $node->bundle(), |
||||||
|
'title' => $node->getTitle(), |
||||||
|
'deleted' => TRUE, |
||||||
|
'deleted_timestamp' => date('c'), |
||||||
|
'_metadata' => [ |
||||||
|
'ocfl_version' => '1.1', |
||||||
|
'export_timestamp' => date('c'), |
||||||
|
'drupal_version' => \Drupal::VERSION, |
||||||
|
'entity_type' => 'node', |
||||||
|
'deletion_marker' => TRUE, |
||||||
|
], |
||||||
|
]; |
||||||
|
|
||||||
|
return $this->saveObject($node->uuid(), $data); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Mark an OCFL object as deleted (for media). |
||||||
|
* |
||||||
|
* @param \Drupal\media\MediaInterface $media |
||||||
|
* The media entity being deleted. |
||||||
|
* |
||||||
|
* @return bool |
||||||
|
* TRUE if marking was successful. |
||||||
|
*/ |
||||||
|
public function markMediaDeleted(\Drupal\media\MediaInterface $media): bool { |
||||||
|
$root = $this->getStorageRoot(); |
||||||
|
$objectPath = $root . '/' . $media->uuid(); |
||||||
|
|
||||||
|
if (!is_dir($objectPath)) { |
||||||
|
return TRUE; |
||||||
|
} |
||||||
|
|
||||||
|
// Create a new version with deletion marker. |
||||||
|
$data = [ |
||||||
|
'uuid' => $media->uuid(), |
||||||
|
'type' => $media->bundle(), |
||||||
|
'name' => $media->getName(), |
||||||
|
'deleted' => TRUE, |
||||||
|
'deleted_timestamp' => date('c'), |
||||||
|
'_metadata' => [ |
||||||
|
'ocfl_version' => '1.1', |
||||||
|
'export_timestamp' => date('c'), |
||||||
|
'drupal_version' => \Drupal::VERSION, |
||||||
|
'entity_type' => 'media', |
||||||
|
'deletion_marker' => TRUE, |
||||||
|
], |
||||||
|
]; |
||||||
|
|
||||||
|
return $this->saveObject($media->uuid(), $data); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the current version number for an object. |
||||||
|
* |
||||||
|
* @param string $objectPath |
||||||
|
* The path to the object directory. |
||||||
|
* |
||||||
|
* @return int |
||||||
|
* The current version number (0 if object doesn't exist). |
||||||
|
*/ |
||||||
|
protected function getCurrentVersion(string $objectPath): int { |
||||||
|
if (!is_dir($objectPath)) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
$inventoryPath = $objectPath . '/inventory.json'; |
||||||
|
if (!file_exists($inventoryPath)) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
$inventory = json_decode(file_get_contents($inventoryPath), TRUE); |
||||||
|
if (!$inventory || !isset($inventory['head'])) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
// Parse version number from head (e.g., "v3" -> 3). |
||||||
|
return (int) substr($inventory['head'], 1); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Generate OCFL inventory for an object. |
||||||
|
* |
||||||
|
* @param string $objectId |
||||||
|
* The object ID. |
||||||
|
* @param string $objectPath |
||||||
|
* The object path. |
||||||
|
* @param int $version |
||||||
|
* The new version number. |
||||||
|
* @param string $nodeJsonPath |
||||||
|
* The path to the new node.json file. |
||||||
|
* |
||||||
|
* @return array |
||||||
|
* The inventory data. |
||||||
|
*/ |
||||||
|
protected function generateInventory(string $objectId, string $objectPath, int $version, string $nodeJsonPath): array { |
||||||
|
$versionDir = 'v' . $version; |
||||||
|
|
||||||
|
// Calculate checksum for the new content file. |
||||||
|
$contentChecksum = hash_file('sha512', $nodeJsonPath); |
||||||
|
|
||||||
|
// Load existing inventory if this is an update. |
||||||
|
$existingManifest = []; |
||||||
|
$existingVersions = []; |
||||||
|
if ($version > 1) { |
||||||
|
$existingInventoryPath = $objectPath . '/inventory.json'; |
||||||
|
if (file_exists($existingInventoryPath)) { |
||||||
|
$existing = json_decode(file_get_contents($existingInventoryPath), TRUE); |
||||||
|
$existingManifest = $existing['manifest'] ?? []; |
||||||
|
$existingVersions = $existing['versions'] ?? []; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Build manifest - maps checksums to file paths. |
||||||
|
$manifest = $existingManifest; |
||||||
|
$contentPath = $versionDir . '/content/node.json'; |
||||||
|
$manifest[$contentChecksum] = [$contentPath]; |
||||||
|
|
||||||
|
// Build version state - maps checksums to logical paths. |
||||||
|
$versionState = [ |
||||||
|
$contentChecksum => ['node.json'], |
||||||
|
]; |
||||||
|
|
||||||
|
// Build versions array. |
||||||
|
$versions = $existingVersions; |
||||||
|
$versions[$versionDir] = [ |
||||||
|
'created' => date('c'), |
||||||
|
'message' => $version === 1 ? 'Initial version' : 'Updated version', |
||||||
|
'state' => $versionState, |
||||||
|
]; |
||||||
|
|
||||||
|
$inventory = [ |
||||||
|
'id' => $objectId, |
||||||
|
'type' => 'https://ocfl.io/1.1/spec/#inventory', |
||||||
|
'digestAlgorithm' => 'sha512', |
||||||
|
'head' => $versionDir, |
||||||
|
'manifest' => $manifest, |
||||||
|
'versions' => $versions, |
||||||
|
]; |
||||||
|
|
||||||
|
return $inventory; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Check if storage is properly initialized. |
||||||
|
* |
||||||
|
* @return bool |
||||||
|
* TRUE if storage is initialized. |
||||||
|
*/ |
||||||
|
public function isInitialized(): bool { |
||||||
|
$root = $this->getStorageRoot(); |
||||||
|
return file_exists($root . '/0=ocfl_1.1') && file_exists($root . '/ocfl_layout.json'); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get object info by UUID. |
||||||
|
* |
||||||
|
* @param string $uuid |
||||||
|
* The object UUID. |
||||||
|
* |
||||||
|
* @return array|null |
||||||
|
* Object info or NULL if not found. |
||||||
|
*/ |
||||||
|
public function getObjectInfo(string $uuid): ?array { |
||||||
|
$root = $this->getStorageRoot(); |
||||||
|
$objectPath = $root . '/' . $uuid; |
||||||
|
$inventoryPath = $objectPath . '/inventory.json'; |
||||||
|
|
||||||
|
if (!file_exists($inventoryPath)) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
return json_decode(file_get_contents($inventoryPath), TRUE); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the path to the latest export file for an object. |
||||||
|
* |
||||||
|
* @param string $uuid |
||||||
|
* The object UUID. |
||||||
|
* |
||||||
|
* @return string|null |
||||||
|
* The absolute path to the latest node.json file, or NULL if not found. |
||||||
|
*/ |
||||||
|
public function getLatestExportPath(string $uuid): ?string { |
||||||
|
$objectInfo = $this->getObjectInfo($uuid); |
||||||
|
if (!$objectInfo || !isset($objectInfo['head'])) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
$root = $this->getStorageRoot(); |
||||||
|
$filePath = $root . '/' . $uuid . '/' . $objectInfo['head'] . '/content/node.json'; |
||||||
|
|
||||||
|
if (!file_exists($filePath)) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
return $filePath; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get export status info for an entity. |
||||||
|
* |
||||||
|
* @param string $uuid |
||||||
|
* The entity UUID. |
||||||
|
* |
||||||
|
* @return array|null |
||||||
|
* Array with 'version', 'path', and 'created' keys, or NULL if not exported. |
||||||
|
*/ |
||||||
|
public function getExportStatus(string $uuid): ?array { |
||||||
|
$objectInfo = $this->getObjectInfo($uuid); |
||||||
|
if (!$objectInfo || !isset($objectInfo['head'])) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
$head = $objectInfo['head']; |
||||||
|
$root = $this->getStorageRoot(); |
||||||
|
$filePath = $root . '/' . $uuid . '/' . $head . '/content/node.json'; |
||||||
|
|
||||||
|
if (!file_exists($filePath)) { |
||||||
|
return NULL; |
||||||
|
} |
||||||
|
|
||||||
|
$created = $objectInfo['versions'][$head]['created'] ?? NULL; |
||||||
|
|
||||||
|
return [ |
||||||
|
'version' => $head, |
||||||
|
'path' => $filePath, |
||||||
|
'created' => $created, |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,96 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Drupal\ocfl_export\Plugin\Action; |
||||||
|
|
||||||
|
use Drupal\Core\Action\ActionBase; |
||||||
|
use Drupal\Core\Plugin\ContainerFactoryPluginInterface; |
||||||
|
use Drupal\Core\Session\AccountInterface; |
||||||
|
use Drupal\media\MediaInterface; |
||||||
|
use Drupal\ocfl_export\MediaSerializerService; |
||||||
|
use Drupal\ocfl_export\OcflStorageService; |
||||||
|
use Psr\Log\LoggerInterface; |
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Exports a media entity to OCFL storage. |
||||||
|
* |
||||||
|
* @Action( |
||||||
|
* id = "ocfl_export_media", |
||||||
|
* label = @Translation("Export to OCFL"), |
||||||
|
* type = "media", |
||||||
|
* confirm = TRUE, |
||||||
|
* ) |
||||||
|
*/ |
||||||
|
class ExportMediaToOcfl extends ActionBase implements ContainerFactoryPluginInterface { |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
array $configuration, |
||||||
|
$plugin_id, |
||||||
|
$plugin_definition, |
||||||
|
protected OcflStorageService $storageService, |
||||||
|
protected MediaSerializerService $serializerService, |
||||||
|
protected LoggerInterface $logger, |
||||||
|
) { |
||||||
|
parent::__construct($configuration, $plugin_id, $plugin_definition); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { |
||||||
|
return new static( |
||||||
|
$configuration, |
||||||
|
$plugin_id, |
||||||
|
$plugin_definition, |
||||||
|
$container->get('ocfl_export.storage'), |
||||||
|
$container->get('ocfl_export.media_serializer'), |
||||||
|
$container->get('logger.factory')->get('ocfl_export'), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function execute(?MediaInterface $media = NULL): void { |
||||||
|
if (!$media) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
$data = $this->serializerService->serialize($media); |
||||||
|
$success = $this->storageService->saveObject($media->uuid(), $data); |
||||||
|
|
||||||
|
if ($success) { |
||||||
|
$this->logger->info('Batch exported media @mid (@name) to OCFL.', [ |
||||||
|
'@mid' => $media->id(), |
||||||
|
'@name' => $media->getName(), |
||||||
|
]); |
||||||
|
} |
||||||
|
else { |
||||||
|
$this->logger->error('Failed to batch export media @mid to OCFL.', [ |
||||||
|
'@mid' => $media->id(), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (\Exception $e) { |
||||||
|
$this->logger->error('Error exporting media @mid to OCFL: @message', [ |
||||||
|
'@mid' => $media->id(), |
||||||
|
'@message' => $e->getMessage(), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE): bool { |
||||||
|
/** @var \Drupal\media\MediaInterface $object */ |
||||||
|
$access = $object->access('view', $account, TRUE) |
||||||
|
->andIf($object->access('update', $account, TRUE)); |
||||||
|
|
||||||
|
return $return_as_object ? $access : $access->isAllowed(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,96 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace Drupal\ocfl_export\Plugin\Action; |
||||||
|
|
||||||
|
use Drupal\Core\Action\ActionBase; |
||||||
|
use Drupal\Core\Plugin\ContainerFactoryPluginInterface; |
||||||
|
use Drupal\Core\Session\AccountInterface; |
||||||
|
use Drupal\node\NodeInterface; |
||||||
|
use Drupal\ocfl_export\NodeSerializerService; |
||||||
|
use Drupal\ocfl_export\OcflStorageService; |
||||||
|
use Psr\Log\LoggerInterface; |
||||||
|
use Symfony\Component\DependencyInjection\ContainerInterface; |
||||||
|
|
||||||
|
/** |
||||||
|
* Exports a node to OCFL storage. |
||||||
|
* |
||||||
|
* @Action( |
||||||
|
* id = "ocfl_export_node", |
||||||
|
* label = @Translation("Export to OCFL"), |
||||||
|
* type = "node", |
||||||
|
* confirm = TRUE, |
||||||
|
* ) |
||||||
|
*/ |
||||||
|
class ExportNodeToOcfl extends ActionBase implements ContainerFactoryPluginInterface { |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
array $configuration, |
||||||
|
$plugin_id, |
||||||
|
$plugin_definition, |
||||||
|
protected OcflStorageService $storageService, |
||||||
|
protected NodeSerializerService $serializerService, |
||||||
|
protected LoggerInterface $logger, |
||||||
|
) { |
||||||
|
parent::__construct($configuration, $plugin_id, $plugin_definition); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { |
||||||
|
return new static( |
||||||
|
$configuration, |
||||||
|
$plugin_id, |
||||||
|
$plugin_definition, |
||||||
|
$container->get('ocfl_export.storage'), |
||||||
|
$container->get('ocfl_export.node_serializer'), |
||||||
|
$container->get('logger.factory')->get('ocfl_export'), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function execute(?NodeInterface $node = NULL): void { |
||||||
|
if (!$node) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
$data = $this->serializerService->serialize($node); |
||||||
|
$success = $this->storageService->saveObject($node->uuid(), $data); |
||||||
|
|
||||||
|
if ($success) { |
||||||
|
$this->logger->info('Batch exported node @nid (@title) to OCFL.', [ |
||||||
|
'@nid' => $node->id(), |
||||||
|
'@title' => $node->getTitle(), |
||||||
|
]); |
||||||
|
} |
||||||
|
else { |
||||||
|
$this->logger->error('Failed to batch export node @nid to OCFL.', [ |
||||||
|
'@nid' => $node->id(), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (\Exception $e) { |
||||||
|
$this->logger->error('Error exporting node @nid to OCFL: @message', [ |
||||||
|
'@nid' => $node->id(), |
||||||
|
'@message' => $e->getMessage(), |
||||||
|
]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* {@inheritdoc} |
||||||
|
*/ |
||||||
|
public function access($object, ?AccountInterface $account = NULL, $return_as_object = FALSE): bool { |
||||||
|
/** @var \Drupal\node\NodeInterface $object */ |
||||||
|
$access = $object->access('view', $account, TRUE) |
||||||
|
->andIf($object->access('update', $account, TRUE)); |
||||||
|
|
||||||
|
return $return_as_object ? $access : $access->isAllowed(); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue