Gets media files from all collection members
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

<?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;
}
}