data_pipelines-1.x-dev/src/Plugin/DatasetDestination/FileDestinationBase.php
src/Plugin/DatasetDestination/FileDestinationBase.php
<?php
declare(strict_types=1);
namespace Drupal\data_pipelines\Plugin\DatasetDestination;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\File\Exception\FileException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\data_pipelines\Destination\DatasetDestinationPluginBase;
use Drupal\data_pipelines\Entity\DatasetInterface;
use Drupal\data_pipelines\Entity\DestinationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for file destination plugins.
*/
abstract class FileDestinationBase extends DatasetDestinationPluginBase implements ContainerFactoryPluginInterface {
/**
* The filesystem service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The stream wrapper manager.
*
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
*/
protected $streamWrapperManager;
/**
* Constructs a new FileDestinationBase.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\File\FileSystemInterface $fileSystem
* The file system service.
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
* The stream wrapper manager.
*/
final public function __construct(array $configuration, $plugin_id, $plugin_definition, FileSystemInterface $fileSystem, StreamWrapperManagerInterface $streamWrapperManager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->fileSystem = $fileSystem;
$this->streamWrapperManager = $streamWrapperManager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('file_system'),
$container->get('stream_wrapper_manager')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return [
'scheme' => 'public',
'dir' => '',
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
$options = $this->streamWrapperManager->getDescriptions(StreamWrapperInterface::WRITE_VISIBLE);
if (!empty($options)) {
$form['scheme'] = [
'#type' => 'radios',
'#title' => $this->t('File scheme'),
'#default_value' => $this->configuration['scheme'],
'#options' => $options,
'#description' => $this->t('The file scheme to use.'),
'#required' => TRUE,
];
}
$form['dir'] = [
'#type' => 'textfield',
'#title' => $this->t('Sub-directory path'),
'#description' => $this->t('(optional) The sub-directory to write files to.'),
'#default_value' => $this->configuration['dir'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function viewSettings(): array {
return [
'data' => [
'#markup' => new TranslatableMarkup("Path: %path", [
'%path' => $this->getDirPath(),
]),
],
];
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void {
$scheme = $form_state->getValue('scheme');
$path = $form_state->getValue('dir');
$fullPath = $scheme . '://' . ltrim($path, '/');
$dirname = $this->fileSystem->dirname($fullPath);
if (!$this->fileSystem->prepareDirectory($dirname, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
$form_state->setErrorByName('dir', $this->t("Destination path is not writable."));
}
$basename = $this->fileSystem->basename($fullPath);
if (!Unicode::validateUtf8($basename)) {
$form_state->setErrorByName('dir', $this->t("Filename must contain valid UTF-8 characters."));
}
}
/**
* Gets the dir.
*
* @return string
* The file path.
*/
public function getDir(): string {
$dir = $this->configuration['dir'];
if (empty($dir)) {
return $dir;
}
// Remove leading slash.
$dir = ltrim($dir, '/');
// Add trailing slash.
return rtrim($dir, '/') . '/';
}
/**
* Gets the file scheme.
*
* @return string|null
* The file scheme.
*/
public function getScheme(): ?string {
return $this->configuration['scheme'];
}
/**
* Gets the file path.
*
* @return string|null
* The file path.
*/
public function getDirPath(): ?string {
if ($this->getScheme()) {
return $this->getScheme() . '://' . $this->getDir();
}
return NULL;
}
/**
* Gets the file path for the dataset.
*
* @param \Drupal\data_pipelines\Entity\DatasetInterface $dataset
* The dataset.
*
* @return string
* The file path.
*/
public function getFilePath(DatasetInterface $dataset): string {
return $this->getDirPath() . $this->getFilename($dataset);
}
/**
* Get the Url object for the file of the dataset.
*
* @param \Drupal\data_pipelines\Entity\DatasetInterface $dataset
* The dataset.
*
* @return \Drupal\Core\Url
* File Url.
*/
public function getFileUrl(DatasetInterface $dataset): Url {
$fileUri = $this->getFilePath($dataset);
$filePath = $this->fileSystem->realpath($fileUri);
// Add the timestamp as a cache-bursting parameter.
$timestamp = file_exists($filePath) ? filemtime($filePath) : time();
return Url::fromUri($fileUri, [
'query' => [
't' => $timestamp,
],
]);
}
/**
* Get the dataset filename.
*
* @param \Drupal\data_pipelines\Entity\DatasetInterface $dataset
* The dataset.
*
* @return string
* The filename.
*/
protected function getFilename(DatasetInterface $dataset): string {
return $dataset->getMachineName() . '.' . $this->getFileExtension();
}
/**
* {@inheritdoc}
*/
public function deleteDataSet(DatasetInterface $dataset, DestinationInterface $destination): bool {
$filePath = $this->getFilePath($dataset);
try {
$this->fileSystem->delete($filePath);
}
catch (FileException $e) {
$this->messenger()
->addError($this->t("Error deleting '%path': %error", [
'%path' => $filePath,
'%error' => $e->getMessage(),
]));
return FALSE;
}
return TRUE;
}
/**
* Serialize the chunk for writing to file.
*
* @param \Drupal\data_pipelines\DatasetData[] $chunk
* Chunk data.
*
* @return string
* The serialized chunk.
*/
abstract protected function serializeChunk(array $chunk): string;
/**
* Gets the file extension.
*
* @return string
* The file extension.
*/
abstract protected function getFileExtension(): string;
/**
* {@inheritdoc}
*/
public function beginProcessing(DatasetInterface $dataset): bool {
parent::beginProcessing($dataset);
$dirPath = $this->getDirPath();
if (!$this->fileSystem->prepareDirectory($dirPath, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
$this->messenger()
->addError($this->t("Could not create directory '%dirname'", [
'%dirname' => $dirPath,
]));
return FALSE;
}
$filePath = $this->getFilePath($dataset);
$this->fileSystem->delete($filePath);
touch($filePath);
return TRUE;
}
/**
* {@inheritdoc}
*/
public function processChunk(DatasetInterface $dataset, array $chunk): bool {
parent::processChunk($dataset, $chunk);
$data = $this->serializeChunk($chunk);
$filepath = $this->getFilePath($dataset);
$file = fopen($filepath, 'a');
try {
fwrite($file, $data);
fclose($file);
}
catch (\Exception $e) {
$this->messenger()
->addError($this->t("Error saving to destination '%path': %error", [
'%path' => $this->getDir(),
'%error' => $e->getMessage(),
]));
return FALSE;
}
return TRUE;
}
}
