diff --git a/css/form-styles.css b/css/form-styles.css index f440e47..47c5af4 100644 --- a/css/form-styles.css +++ b/css/form-styles.css @@ -1,13 +1,13 @@ -/* Use Flexbox to align form fields side by side */ .media-fields-wrapper { display: flex; - gap: 20px; /* Space between elements */ - align-items: center; + gap: 20px; + align-items: flex-start; + flex-wrap: wrap; + /* Optional, improves mobile responsiveness */ } -/* Ensure each field takes up equal space */ -.media-side-by-side { - flex: 1; - min-width: 200px; /* Prevents elements from getting too small */ +.media-field-container { + width: 250px; + flex: 0 0 auto; } diff --git a/islandora_inplace_media.routing.yml b/islandora_inplace_media.routing.yml index b41c873..f0995d3 100644 --- a/islandora_inplace_media.routing.yml +++ b/islandora_inplace_media.routing.yml @@ -1,7 +1,17 @@ islandora_inplace_media.create_media_from_file: path: '/islandora-inplace-media/create-media-from-file' defaults: - _title: 'Create Media From File' + _title: 'Create Media From Files' _form: 'Drupal\islandora_inplace_media\Form\CreateMediaFromFileForm' requirements: _permission: 'access content' + +islandora_inplace_media.instructions_modal: + path: '/instructions-modal' + defaults: + _controller: '\Drupal\islandora_inplace_media\Controller\InstructionsController::modal' + _title: 'Instructions' + requirements: + _permission: 'access content' + options: + _format: 'html' diff --git a/src/Controller/InstructionsController.php b/src/Controller/InstructionsController.php new file mode 100644 index 0000000..8b75480 --- /dev/null +++ b/src/Controller/InstructionsController.php @@ -0,0 +1,46 @@ +Upload your files to the server. If the files use the naming convention of {nid}_filename the created media will be + associated with the node identified by that nid. If not, the media will still be created, but will have to be associated + manually to a node through the UI. +

+

If Already in place is selected, files in the destination folder will become Drupal managed files, + and those files will be used to build the media. This is the fastest and most efficient way to ingest very large files.

+

The source directory holds files to be ingested. The source directory must be identified by the full + server path e.g. /var/www/upload_folder. +

+

The destination directory is within the Drupal file system. The path is relative, and the folder will be created if + it does not already exist.

+

The file storage system is defined within the Media Types definitions. The uploaded files will be stored + in the default directory unless another file system is selected. +

+

File ownership is normally www-data:www-data on Apache systems and nginx:nginx for + a Docker based Islandora installation. The default value is normally fine, but check with your systems administrator + if you are unsure. +

"; + return [ + '#type' => 'markup', + '#markup' => $html, + '#attached' => [ + 'library' => [ + 'core/drupal.dialog', + ], + ], + ]; + } + +} diff --git a/src/Form/CreateMediaFromFileForm.php b/src/Form/CreateMediaFromFileForm.php index 66435e6..54932bf 100644 --- a/src/Form/CreateMediaFromFileForm.php +++ b/src/Form/CreateMediaFromFileForm.php @@ -4,11 +4,15 @@ declare(strict_types=1); namespace Drupal\islandora_inplace_media\Form; +use Drupal\Core\StreamWrapper\StreamWrapperInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\islandora_inplace_media\Utils; use Drupal\Core\Entity\EntityTypeManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Url; +use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface; +use Drupal\field\Entity\FieldConfig; /** * Provides an Islandora Inplace Media form. @@ -29,12 +33,21 @@ final class CreateMediaFromFileForm extends FormBase { */ protected $entityTypeManager; + /** + * The stream wrapper service. + * + * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface + */ + + protected $streamWrapperManager; + /** * {@inheritdoc} */ - public function __construct(Utils $mediaUtils, EntityTypeManagerInterface $entityTypeManager) { + public function __construct(Utils $mediaUtils, EntityTypeManagerInterface $entityTypeManager, StreamWrapperManagerInterface $streamWrapperManager) { $this->mediaUtils = $mediaUtils; $this->entityTypeManager = $entityTypeManager; + $this->streamWrapperManager = $streamWrapperManager; } /** @@ -43,7 +56,8 @@ final class CreateMediaFromFileForm extends FormBase { public static function create(ContainerInterface $container): self { return new self( $container->get('islandora_inplace_media.utils'), - $container->get('entity_type.manager') + $container->get('entity_type.manager'), + $container->get('stream_wrapper_manager') ); } @@ -58,6 +72,13 @@ final class CreateMediaFromFileForm extends FormBase { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state): array { + $server_software = $_SERVER['SERVER_SOFTWARE'] ?? ''; + $default_ownership = match (TRUE) { + stripos($server_software, 'apache') !== FALSE => 'www-data:www-data', + stripos($server_software, 'nginx') !== FALSE => 'nginx:nginx', + default => 'unknown:unknown', + }; + $vid = 'islandora_media_use'; $terms = $this->entityTypeManager->getStorage('taxonomy_term')->loadTree($vid); $media_use_options = []; @@ -68,46 +89,112 @@ final class CreateMediaFromFileForm extends FormBase { $term_default = $term->tid; } } + $wrappers = $this->streamWrapperManager->getWrappers(); + $file_system_options = []; + $wrappers = $this->streamWrapperManager->getWrappers(); + $file_system_options = []; + $file_system_options['default'] = $this->t("File system defined in media type"); + + $unwanted = ['temporary', 'assets']; + foreach ($wrappers as $scheme => $wrapper_info) { + if (in_array($scheme, $unwanted)) { + continue; + } + $class = $wrapper_info['class']; + if ( + is_a($class, StreamWrapperInterface::class, TRUE) && + class_exists($class) + ) { + $instance = new $class(); + $instance->setUri($scheme . '://'); + $file_system_options[$scheme] = $instance->getName() ?: $scheme; + } + } $media_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple(); $types = array_map(fn($media_type) => $media_type->label(), $media_types); + $form['instructions'] = [ + '#type' => 'link', + '#title' => $this->t('View Instructions'), + '#url' => Url::fromRoute('islandora_inplace_media.instructions_modal'), + '#attributes' => [ + 'class' => ['use-ajax', 'button', 'button--small'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'width' => 700, + ]), + ], + '#attached' => [ + 'library' => [ + 'core/drupal.dialog.ajax', + ], + ], + ]; + $form['in_place'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Already in place?'), + '#description' => $this->t("Check this box if files are already in the file system directory."), + ]; + $form['source'] = [ '#type' => 'textfield', - '#title' => $this->t('Enter full file path to input directory'), - '#required' => TRUE, + '#title' => $this->t('Enter the full file path to the input directory'), + '#states' => [ + 'visible' => [ + ':input[name="in_place"]' => ['checked' => FALSE], + ], + 'required' => [ + ':input[name="in_place"]' => ['checked' => FALSE], + ], + ], ]; - $form['directory'] = [ + + $form['destination'] = [ '#type' => 'textfield', - '#title' => $this->t('Enter the destination directory'), + '#title' => $this->t('Enter the directory on the file system'), '#required' => TRUE, ]; - $form['destination'] = [ + $form['file_system'] = [ '#type' => 'radios', - '#options' => [ - 'public' => $this->t('Public'), - 'private' => $this->t('Private'), - ], + '#options' => $file_system_options, '#title' => $this->t('Select file system'), '#required' => TRUE, - '#default_value' => 'public', + '#default_value' => 'default', ]; $form['field_wrapper'] = [ '#type' => 'container', '#attributes' => ['class' => ['media-fields-wrapper']], ]; - $form['field_wrapper']['media_type'] = [ + $form['field_wrapper']['media_type_wrapper'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['media-field-container']], + ]; + $form['field_wrapper']['media_type_wrapper']['media_type'] = [ '#title' => $this->t("Media type."), '#type' => 'select', '#options' => $types, '#attributes' => ['class' => ['media-side-by-side']], ]; - $form['field_wrapper']['media_use'] = [ + $form['field_wrapper']['media_use_wrapper'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['media-field-container']], + ]; + $form['field_wrapper']['media_use_wrapper']['media_use'] = [ '#title' => $this->t("Media use."), '#type' => 'select', '#options' => $media_use_options, '#default_value' => $term_default, '#attributes' => ['class' => ['media-side-by-side']], ]; + $form['field_wrapper']['ownership_wrapper'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['media-field-container']], + ]; + $form['field_wrapper']['ownership_wrapper']['ownership'] = [ + '#type' => 'textfield', + '#title' => $this->t('File ownership'), + '#default_value' => $default_ownership, + ]; $form['actions'] = [ '#type' => 'actions', @@ -125,29 +212,69 @@ final class CreateMediaFromFileForm extends FormBase { * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state): void { - $source_path = $form_state->getValue('source'); - - if (!is_dir($source_path)) { + $file_system = $form_state->getValue('file_system') . "://"; + $source_directory = $form_state->getValue('in_place') ? $file_system . $form_state->getValue('destination') : $form_state->getValue('source'); + if (!is_dir($source_directory)) { $form_state->setErrorByName('source', $this->t('The specified directory does not exist.')); } + if (is_dir($source_directory)) { + $files = scandir($source_directory); + $files = array_diff($files, ['.', '..']); + if (!$files) { + $form_state->setErrorByName('source', $this->t('The specified directory is empty.')); + } + } } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state): void { - $source_path = $form_state->getValue('source'); - $directory = trim($form_state->getValue('directory'), '/'); - $file_system = $form_state->getValue('destination') === 'public' ? 'public://' : 'private://'; - $destination = "{$file_system}{$directory}"; + $file_types = [ + 'audio' => 'field_media_audio_file', + 'document' => 'field_media_document', + 'file' => 'field_media_file', + 'image' => 'field_media_image', + 'video' => 'field_media_video_file', + ]; $media_type = $form_state->getValue('media_type'); - $media_use = $form_state->getValue('media_use'); - $this->mediaUtils->buildMediaBatch($source_path, $destination, $media_type, $media_use); - $this->messenger()->addStatus($this->t('Media files have been processed from %source to %destination.', [ - '%source' => $source_path, - '%destination' => $destination, - ])); + $file_type = $file_types[$media_type]; + + if ($form_state->getValue('file_system') == 'default') { + $field_config = FieldConfig::loadByName('media', $media_type, $file_type); + if ($field_config) { + $settings = $field_config->getSettings(); + $scheme = $settings['uri_scheme'] ?? 'public'; + $file_system = $scheme . "://"; + } + } + else { + $file_system = $form_state->getValue('file_system') . "://"; + } + + $destination = trim($form_state->getValue('destination'), '/'); + $build_data = [ + 'source_dir' => $form_state->getValue('in_place') ? $file_system . $destination : $form_state->getValue('source'), + 'destination_path' => "{$file_system}{$destination}", + 'media_type' => $media_type, + 'file_type' => $file_type, + 'media_use' => $form_state->getValue('media_use'), + 'ownership' => $form_state->getValue('ownership'), + ]; + $this->mediaUtils->buildMediaBatch($build_data); + if ($form_state->getValue('in_place')) { + $status = $this->t('Media files in %destination have been processed', [ + '%destination' => $destination, + ]); + } + else { + $status = $this->t('Media files have been processed from %source to %destination.', [ + '%source' => $build_data['source_dir'], + '%destination' => $build_data['destination_path'], + ]); + } + $this->messenger()->addStatus($status); $form_state->setRedirect(''); } diff --git a/src/Utils.php b/src/Utils.php index 1dcae55..70cc118 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -16,75 +16,76 @@ class Utils { /** * Processes individual file. */ - public static function processMediaBatch($file_name, $dir, $dest, $media_type, $media_use_tid, &$context) { + public static function processMediaBatch($file_name, $build_data, &$context) { if (!isset($context['results'])) { $context['results'] = []; } - $file_types = [ - 'audio' => 'field_media_audio_file', - 'document' => 'field_media_document', - 'file' => 'field_media_file', - 'image' => 'field_media_image', - 'video' => 'field_media_video_file', - ]; - $file_type = $file_types[$media_type] ?? 'field_media_file'; - + $media_type = $build_data['media_type']; + $file_type = $build_data['file_type'] ?? 'field_media_file'; $logger = \Drupal::logger('islandora_inplace_media'); $fileSystem = \Drupal::service('file_system'); - - $source_path = $dir . '/' . $file_name; - $destination_path = $dest . '/' . $file_name; + $source_path = $build_data['source_dir'] . '/' . $file_name; + $destination_path = $build_data['destination_path'] . '/' . $file_name; if (!\file_exists($source_path)) { $logger->warning('File does not exist: @file', ['@file' => $source_path]); return; } + if ($source_path === $destination_path) { + $path = $destination_path; + } + else { + $path = $fileSystem->copy($source_path, $destination_path, FileSystemInterface::EXISTS_RENAME); + } + $file = \Drupal::service('file.repository')->loadByUri($path); + if (!$file) { + $absolute_path = $fileSystem->realpath($destination_path); + if ($absolute_path) { + chown($absolute_path, $build_data['ownership']); + } + + $file = File::create([ + 'uri' => $path, + 'status' => 1, + ]); + $file->save(); + } - $moved_file = $fileSystem->move($source_path, $destination_path, FileSystemInterface::EXISTS_RENAME); - $absolute_path = $fileSystem->realpath($destination_path); - chown($absolute_path, 'www-data'); - chgrp($absolute_path, 'lib-dev'); - $new_file = File::create([ - 'uri' => $moved_file, - 'status' => 1, - ]); - $new_file->save(); - $context['results'][$new_file->id()] = $moved_file; + $context['results'][$file->id()] = $path; if (preg_match('/^(\d+)_/', $file_name, $matches)) { $nid = $matches[1] ?? NULL; } - if ($nid) { - $media = Media::create([ - "bundle" => $media_type, - "name" => $file_name, - $file_type => [ - "target_id" => $new_file->id(), - ], - 'field_media_use' => [ - "target_id" => $media_use_tid, - ], - "field_media_of" => $nid, - ]); - $media->save(); - } + $media = Media::create([ + "bundle" => $media_type, + "name" => $file_name, + $file_type => [ + "target_id" => $file->id(), + ], + 'field_media_use' => [ + "target_id" => $build_data['media_use'], + ], + "field_media_of" => $nid, + ]); + $media->save(); + } /** * Builds batch from input directory. */ - public static function buildMediaBatch(string $dir, string $dest, string $media_type, string $media_use_tid) { - if (!is_dir($dir)) { - \Drupal::logger('islandora_inplace_media')->error('Source directory does not exist: @dir', ['@dir' => $dir]); + public static function buildMediaBatch($build_data) { + $source_dir = $build_data['source_dir']; + if ($source_dir && !is_dir($source_dir)) { + \Drupal::logger('islandora_inplace_media')->error('Source directory does not exist: @dir', ['@dir' => $source_dir]); return; } - - // Use a service within the function, not in the batch context. + $destination = $build_data['destination_path']; $fileSystem = \Drupal::service('file_system'); - $fileSystem->prepareDirectory($dest, FileSystemInterface::CREATE_DIRECTORY); + $fileSystem->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY); - $files = scandir($dir); + $files = scandir($source_dir); $files = array_diff($files, ['.', '..']); $batch = [ @@ -97,7 +98,7 @@ class Utils { foreach ($files as $file_name) { $batch['operations'][] = [ [self::class, 'processMediaBatch'], - [$file_name, $dir, $dest, $media_type, $media_use_tid], + [$file_name, $build_data], ]; }