You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
215 lines
6.8 KiB
215 lines
6.8 KiB
<?php |
|
|
|
declare(strict_types=1); |
|
|
|
namespace Drupal\islandora_collection_harvest\Controller; |
|
|
|
use Drupal\Core\Controller\ControllerBase; |
|
use Drupal\Core\File\FileSystemInterface; |
|
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface; |
|
use Drupal\Core\TempStore\PrivateTempStoreFactory; |
|
use Drupal\flysystem\FlysystemFactory; |
|
use Symfony\Component\HttpFoundation\StreamedResponse; |
|
use Symfony\Component\DependencyInjection\ContainerInterface; |
|
use ZipStream\ZipStream; |
|
use Drupal\media\MediaInterface; |
|
use Drupal\file\FileInterface; |
|
|
|
/** |
|
* Controller responsible for streaming ZIP downloads of collection media. |
|
* |
|
* This class: |
|
* - Loads media entities stored by the form in PrivateTempStore. |
|
* - Builds and streams a ZIP archive using ZipStream without |
|
* temporary files or memory-heavy buffering. |
|
* - Supports both local and Flysystem (remote) file schemes. |
|
* |
|
* @see \Drupal\islandora_collection_harvest\Form\CollectionHarvestForm |
|
*/ |
|
final class HarvestDownloadController extends ControllerBase { |
|
|
|
/** |
|
* The file system service. |
|
* |
|
* @var \Drupal\Core\File\FileSystemInterface |
|
*/ |
|
protected FileSystemInterface $fileSystem; |
|
|
|
/** |
|
* The private tempstore factory. |
|
* |
|
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory |
|
*/ |
|
protected PrivateTempStoreFactory $tempStoreFactory; |
|
|
|
/** |
|
* The Flysystem factory service. |
|
* |
|
* @var \Drupal\flysystem\FlysystemFactory |
|
*/ |
|
protected FlysystemFactory $flysystemFactory; |
|
|
|
/** |
|
* The stream wrapper manager service. |
|
* |
|
* @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface |
|
*/ |
|
protected StreamWrapperManagerInterface $streamWrapperManager; |
|
|
|
/** |
|
* Constructs a new HarvestDownloadController. |
|
* |
|
* @param \Drupal\Core\File\FileSystemInterface $file_system |
|
* The file system service. |
|
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $tempStoreFactory |
|
* The private tempstore factory. |
|
* @param \Drupal\flysystem\FlysystemFactory $flysystemFactory |
|
* The Flysystem factory. |
|
* @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager |
|
* The stream wrapper manager. |
|
*/ |
|
public function __construct( |
|
FileSystemInterface $file_system, |
|
PrivateTempStoreFactory $tempStoreFactory, |
|
FlysystemFactory $flysystemFactory, |
|
StreamWrapperManagerInterface $streamWrapperManager |
|
) { |
|
$this->fileSystem = $file_system; |
|
$this->tempStoreFactory = $tempStoreFactory; |
|
$this->flysystemFactory = $flysystemFactory; |
|
$this->streamWrapperManager = $streamWrapperManager; |
|
} |
|
|
|
/** |
|
* {@inheritdoc} |
|
*/ |
|
public static function create(ContainerInterface $container): self { |
|
return new self( |
|
$container->get('file_system'), |
|
$container->get('tempstore.private'), |
|
$container->get('flysystem_factory'), |
|
$container->get('stream_wrapper_manager') |
|
); |
|
} |
|
|
|
/** |
|
* Streams a ZIP file containing the requested media files. |
|
* |
|
* The media IDs were stored in PrivateTempStore by the form submit handler. |
|
* This method: |
|
* - Reloads all referenced media entities. |
|
* - Iterates through each file, adding it to a ZipStream stream. |
|
* - Handles both local and remote file systems gracefully. |
|
* |
|
* @param string $filename |
|
* The generated filename (used as the PrivateTempStore key and download name). |
|
* |
|
* @return \Symfony\Component\HttpFoundation\StreamedResponse |
|
* The streaming ZIP response. |
|
* |
|
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException |
|
* Thrown if the media list cannot be found in tempstore. |
|
*/ |
|
public function download(string $filename): StreamedResponse { |
|
set_time_limit(0); |
|
ignore_user_abort(true); |
|
|
|
// Retrieve stored media IDs. |
|
$temp_store = $this->tempStoreFactory->get('islandora_collection_harvest'); |
|
$media_ids = $temp_store->get($filename); |
|
|
|
if (empty($media_ids)) { |
|
throw new \Symfony\Component\HttpKernel\Exception\NotFoundHttpException('No media found.'); |
|
} |
|
|
|
// Reload media entities for this request. |
|
/** @var \Drupal\media\MediaInterface[] $media_entities */ |
|
$media_entities = \Drupal::entityTypeManager() |
|
->getStorage('media') |
|
->loadMultiple($media_ids); |
|
|
|
// Build the streamed ZIP response. |
|
$response = new StreamedResponse(function () use ($media_entities) { |
|
// Clean any buffered output. |
|
if (ob_get_level()) { |
|
@ob_end_clean(); |
|
} |
|
|
|
ignore_user_abort(true); |
|
set_time_limit(0); |
|
|
|
// Initialize the ZipStream object. |
|
$zip = new ZipStream(); |
|
|
|
// Process each media entity. |
|
foreach ($media_entities as $media) { |
|
if (!$media instanceof MediaInterface) { |
|
continue; |
|
} |
|
|
|
$source_field = $media->getSource()->getConfiguration()['source_field'] ?? NULL; |
|
if (!$source_field || !$media->hasField($source_field) || $media->get($source_field)->isEmpty()) { |
|
continue; |
|
} |
|
|
|
/** @var \Drupal\file\FileInterface|null $file */ |
|
$file = $media->get($source_field)->entity; |
|
if (!$file instanceof FileInterface) { |
|
continue; |
|
} |
|
|
|
$uri = $file->getFileUri(); |
|
$name_in_zip = basename($uri); |
|
|
|
// Try to use a local file path first. |
|
$realpath = $this->fileSystem->realpath($uri); |
|
if ($realpath && file_exists($realpath)) { |
|
$zip->addFileFromPath($name_in_zip, $realpath); |
|
continue; |
|
} |
|
|
|
// Fall back to Flysystem (remote storage). |
|
try { |
|
$scheme = $this->streamWrapperManager->getScheme($uri); |
|
$path = substr($uri, strlen($scheme) + 3); |
|
$filesystem = $this->flysystemFactory->getFilesystem($scheme); |
|
|
|
if ($filesystem && $filesystem->has($path)) { |
|
// Try stream-based read first. |
|
if (method_exists($filesystem, 'readStream')) { |
|
$stream = $filesystem->readStream($path); |
|
if ($stream) { |
|
$zip->addFileFromStream($name_in_zip, $stream); |
|
@fclose($stream); |
|
} |
|
} |
|
else { |
|
// Fallback to full read (less memory-efficient). |
|
$contents = $filesystem->read($path); |
|
if ($contents !== false) { |
|
$zip->addFile($name_in_zip, $contents); |
|
} |
|
} |
|
} |
|
} |
|
catch (\Exception $e) { |
|
$this->logger('islandora_collection_harvest')->error( |
|
'Flysystem read failed for @uri: @msg', |
|
['@uri' => $uri, '@msg' => $e->getMessage()] |
|
); |
|
} |
|
} |
|
|
|
// Finalize the ZIP output and flush to browser. |
|
$zip->finish(); |
|
@flush(); |
|
}); |
|
|
|
// Set appropriate download headers. |
|
$response->headers->set('Content-Type', 'application/zip'); |
|
$response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"'); |
|
|
|
return $response; |
|
} |
|
|
|
}
|
|
|