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