diff --git a/Services/FileDelivery/classes/class.ilSecureTokenSrcBuilder.php b/Services/FileDelivery/classes/class.ilSecureTokenSrcBuilder.php index 1622e745e5fd..df0ef2387e4f 100644 --- a/Services/FileDelivery/classes/class.ilSecureTokenSrcBuilder.php +++ b/Services/FileDelivery/classes/class.ilSecureTokenSrcBuilder.php @@ -17,12 +17,10 @@ declare(strict_types=1); -use ILIAS\GlobalScreen\Scope\MainMenu\Collector\Renderer\Hasher; use ILIAS\ResourceStorage\Consumer\InlineSrcBuilder; use ILIAS\ResourceStorage\Consumer\SrcBuilder; use ILIAS\ResourceStorage\Flavour\Flavour; use ILIAS\ResourceStorage\Revision\Revision; -use ILIAS\ResourceStorage\StorageHandler\StorageHandler; use ILIAS\FileDelivery\Delivery\Disposition; use ILIAS\FileDelivery\Services; @@ -39,14 +37,18 @@ public function __construct( $this->inline = new InlineSrcBuilder($file_delivery); } - public function getRevisionURL(Revision $revision, bool $signed = true, float $valid_for_at_least_minutes = 60.0): string - { + public function getRevisionURL( + Revision $revision, + bool $signed = true, + float $valid_for_at_least_minutes = 60.0, + string $filename = null + ): string { // get stream from revision $stream = $revision->maybeStreamResolver()?->getStream(); return (string) $this->file_delivery->buildTokenURL( $stream, - $revision->getTitle(), + $filename ?? $revision->getTitle(), Disposition::INLINE, $GLOBALS['ilUser']->getId() ?? 0, (int) (ceil($valid_for_at_least_minutes / 60)) diff --git a/src/Filesystem/Stream/Stream.php b/src/Filesystem/Stream/Stream.php index a386c8ae3e2f..c1a33c2ab2d0 100644 --- a/src/Filesystem/Stream/Stream.php +++ b/src/Filesystem/Stream/Stream.php @@ -98,11 +98,11 @@ public function __construct($stream, StreamOptions $options = null) $this->readable = array_key_exists( $mode, self::$accessMap - ) && (bool) (self::$accessMap[$mode]&self::MASK_ACCESS_READ); + ) && (bool) (self::$accessMap[$mode] & self::MASK_ACCESS_READ); $this->writeable = array_key_exists( $mode, self::$accessMap - ) && (bool) (self::$accessMap[$mode]&self::MASK_ACCESS_WRITE); + ) && (bool) (self::$accessMap[$mode] & self::MASK_ACCESS_WRITE); $this->seekable = $meta['seekable']; $this->uri = $this->getMetadata('uri'); } @@ -150,7 +150,7 @@ public function getSize(): ?int clearstatcache(true, $this->uri); } - $stats = fstat($this->stream); + $stats = fstat($this->stream) ?: []; if (array_key_exists('size', $stats)) { $this->size = $stats['size']; return $this->size; @@ -163,7 +163,7 @@ public function getSize(): ?int /** * @inheritDoc */ - public function tell(): int|bool + public function tell(): int { $this->assertStreamAttached(); @@ -229,7 +229,7 @@ public function isWritable(): bool /** * @inheritDoc */ - public function write($string): int|bool + public function write($string): int { $this->assertStreamAttached(); @@ -259,7 +259,7 @@ public function isReadable(): bool /** * @inheritDoc */ - public function read($length): string|bool + public function read($length): string { $this->assertStreamAttached(); @@ -286,7 +286,7 @@ public function read($length): string|bool /** * @inheritDoc */ - public function getContents(): string|bool + public function getContents(): string { $this->assertStreamAttached(); diff --git a/src/Filesystem/Util/Archive/Archives.php b/src/Filesystem/Util/Archive/Archives.php index e5b35b7c1247..1acea9d60bbc 100644 --- a/src/Filesystem/Util/Archive/Archives.php +++ b/src/Filesystem/Util/Archive/Archives.php @@ -45,6 +45,10 @@ public function __construct() public function zip(array $file_streams, ?ZipOptions $zip_options = null): Zip { + if (empty($file_streams)) { + $file_streams = [Zip::DOT_EMPTY => Streams::ofString('')]; + } + return new Zip( $this->mergeZipOptions($zip_options), ...$file_streams diff --git a/src/Filesystem/Util/Archive/Unzip.php b/src/Filesystem/Util/Archive/Unzip.php index 4e2893c198fe..9a06e213a021 100644 --- a/src/Filesystem/Util/Archive/Unzip.php +++ b/src/Filesystem/Util/Archive/Unzip.php @@ -22,8 +22,6 @@ use ILIAS\Filesystem\Stream\FileStream; use ILIAS\Filesystem\Stream\Streams; -use ILIAS\ResourceStorage\StorageHandler\StorageHandlerFactory; -use ILIAS\ResourceStorage\Resource\StorableResource; use ILIAS\Filesystem\Util; /** @@ -127,6 +125,10 @@ public function getDirectories(): \Generator foreach ($this->getPaths() as $path) { if (substr($path, -1) === self::DS_UNIX || substr($path, -1) === self::DS_WIN) { $directories[] = $path; + continue; + } + if ((str_contains($path, self::DS_UNIX) || str_contains($path, self::DS_WIN))) { + $directories[] = dirname($path) . self::DIRECTORY_SEPARATOR; } } @@ -134,7 +136,7 @@ public function getDirectories(): \Generator foreach ($directories as $directory) { $parent = dirname($directory) . self::DIRECTORY_SEPARATOR; - if ($parent !== self::BASE_DIR . self::DIRECTORY_SEPARATOR && !in_array($parent, $directories)) { + if ($parent !== self::BASE_DIR . self::DIRECTORY_SEPARATOR && !in_array($parent, $directories, true)) { $directories_with_parents[] = $parent; } $directories_with_parents[] = $directory; @@ -207,8 +209,8 @@ public function extract(): bool case ZipDirectoryHandling::ENSURE_SINGLE_TOP_DIR: // top directory with same name as the ZIP without suffix $zip_path = $this->stream->getMetadata(self::URI); - $sufix = '.' . pathinfo($zip_path, PATHINFO_EXTENSION); - $top_directory = basename($zip_path, $sufix); + $sufix = '.' . pathinfo((string) $zip_path, PATHINFO_EXTENSION); + $top_directory = basename((string) $zip_path, $sufix); // first we check if the ZIP contains the top directory $has_top_directory = true; @@ -222,18 +224,16 @@ public function extract(): bool } break; case ZipDirectoryHandling::FLAT_STRUCTURE: - if (!is_dir($destination_path)) { - if (!mkdir($destination_path, 0777, true) && !is_dir($destination_path)) { - throw new \RuntimeException(sprintf('Directory "%s" was not created', $destination_path)); - } + if (!is_dir($destination_path) && (!mkdir($destination_path, 0777, true) && !is_dir($destination_path))) { + throw new \RuntimeException(sprintf('Directory "%s" was not created', $destination_path)); } foreach ($this->getStreams() as $stream) { $uri = $stream->getMetadata(self::URI); - if (substr($uri, -1) === self::DIRECTORY_SEPARATOR) { + if (substr((string) $uri, -1) === self::DIRECTORY_SEPARATOR) { continue; // Skip directories } - $file_name = Util::sanitizeFileName($destination_path . self::DIRECTORY_SEPARATOR . basename($uri)); + $file_name = Util::sanitizeFileName($destination_path . self::DIRECTORY_SEPARATOR . basename((string) $uri)); file_put_contents( $file_name, $stream->getContents() diff --git a/src/Filesystem/Util/Archive/Zip.php b/src/Filesystem/Util/Archive/Zip.php index 942a0a0918ff..24eb29d5de05 100644 --- a/src/Filesystem/Util/Archive/Zip.php +++ b/src/Filesystem/Util/Archive/Zip.php @@ -20,14 +20,9 @@ namespace ILIAS\Filesystem\Util\Archive; -use ILIAS\ResourceStorage\Identification\ResourceIdentification; -use ILIAS\ResourceStorage\StorageHandler\StorageHandler; -use ILIAS\ResourceStorage\StorageHandler\StorageHandlerFactory; -use ILIAS\ResourceStorage\Revision\Revision; -use ILIAS\ResourceStorage\Resource\StorableResource; +use ILIAS\Filesystem\Stream\Stream; use ILIAS\Filesystem\Stream\Streams; use ILIAS\Filesystem\Stream\FileStream; -use ILIAS\Filesystem\Util\Archive\BaseZip; /** * @author Fabian Schmid @@ -36,6 +31,7 @@ class Zip { use PathHelper; + public const DOT_EMPTY = '.empty'; private string $zip_output_file = ''; protected \ZipArchive $zip; private int $iteration_limit; @@ -51,9 +47,7 @@ public function __construct( protected ZipOptions $options, ...$streams ) { - $this->streams = array_filter($streams, function ($stream): bool { - return $stream instanceof FileStream; - }); + $this->streams = array_filter($streams, fn($stream): bool => $stream instanceof FileStream); if ($options->getZipOutputPath() !== null && $options->getZipOutputName() !== null) { $this->zip_output_file = $this->ensureDirectorySeperator( @@ -69,14 +63,10 @@ public function __construct( } $system_limit = (int) shell_exec('ulimit -n') ?: 0; - if ($system_limit < 10) { // aka we cannot determine the system limit properly - $this->iteration_limit = 100; - } else { - $this->iteration_limit = min( - $system_limit / 2, - 5000 - ); - } + $this->iteration_limit = $system_limit < 10 ? 100 : min( + $system_limit / 2, + 5000 + ); $this->zip = new \ZipArchive(); if (!file_exists($this->zip_output_file)) { @@ -113,7 +103,7 @@ private function storeZIPtoFilesystem(): void $this->zip->open($this->zip_output_file); } if (is_int($path_inside_zip)) { - $path_inside_zip = basename($path); + $path_inside_zip = basename((string) $path); } if ($path === 'php://memory') { @@ -138,7 +128,7 @@ private function storeZIPtoFilesystem(): void } } - public function get(): \ILIAS\Filesystem\Stream\Stream + public function get(): Stream { $this->storeZIPtoFilesystem(); @@ -155,14 +145,24 @@ public function get(): \ILIAS\Filesystem\Stream\Stream */ public function addPath(string $path, ?string $path_inside_zip = null): void { + $path_inside_zip = $path_inside_zip ?? basename($path); + + // create directory if it does not exist + $this->zip->addEmptyDir(rtrim(dirname($path_inside_zip), '/') . '/'); + $this->addStream( - Streams::ofResource(fopen($path, 'r')), - $path_inside_zip ?? basename($path) + Streams::ofResource(fopen($path, 'rb')), + $path_inside_zip ); } public function addStream(FileStream $stream, string $path_inside_zip): void { + // we remove the "empty zip file" now if possible + if (count($this->streams) === 1 && isset($this->streams[self::DOT_EMPTY])) { + unset($this->streams[self::DOT_EMPTY]); + } + // we must store the ZIP to e temporary files every 1000 files, otherwise we will get a Too Many Open Files error $this->streams[$path_inside_zip] = $stream; @@ -193,7 +193,6 @@ public function addDirectory(string $directory_to_zip): void \RecursiveIteratorIterator::SELF_FIRST ); - switch ($this->options->getDirectoryHandling()) { case ZipDirectoryHandling::KEEP_STRUCTURE: $pattern = null; @@ -205,7 +204,6 @@ public function addDirectory(string $directory_to_zip): void break; } - foreach ($files as $file) { $pathname = $file->getPathname(); $path_inside_zip = str_replace($directory_to_zip . '/', '', $pathname); @@ -216,7 +214,8 @@ public function addDirectory(string $directory_to_zip): void /** @var $file \SplFileInfo */ if ($file->isDir()) { // add directory to zip if it's empty - if (count(scandir($pathname)) === 2) { + $sub_items = array_filter(scandir($pathname), static fn($d): bool => !str_contains((string) $d, '.DS_Store')); + if (count($sub_items) === 2) { $this->zip->addEmptyDir($path_inside_zip); } continue; diff --git a/src/Filesystem/Util/Convert/ImageConverter.php b/src/Filesystem/Util/Convert/ImageConverter.php index 5fd34be1617c..83c6e187203c 100644 --- a/src/Filesystem/Util/Convert/ImageConverter.php +++ b/src/Filesystem/Util/Convert/ImageConverter.php @@ -173,17 +173,13 @@ protected function handleFormatAndQuality(): void self::RESOLUTION, self::RESOLUTION ); - try { - $this->image->resampleImage( - self::RESOLUTION, - self::RESOLUTION, - \Imagick::FILTER_LANCZOS, - 1 - ); - } catch (\Throwable $t) { - // Cannot resample image, continue without resampling - } - + // High density images do not support Resampling, cache to small. we deactivate this + /*$this->image->resampleImage( + self::RESOLUTION, + self::RESOLUTION, + \Imagick::FILTER_LANCZOS, + 1 + );*/ $quality = $this->output_options->getQuality(); // if $this->output_options->getFormat() is 'keep', we map it to the original format @@ -221,7 +217,7 @@ protected function handleFormatAndQuality(): void $this->image->setImageAlphaChannel(\Imagick::ALPHACHANNEL_ACTIVATE); } $this->image->setImageFormat('png'); - $this->image->setOption('png:compression-level', (string)$png_compression_level); + $this->image->setOption('png:compression-level', (string) $png_compression_level); break; } $this->image->stripImage(); diff --git a/src/ResourceStorage/Consumer/ConsumerFactory.php b/src/ResourceStorage/Consumer/ConsumerFactory.php index 908e43ef0917..cf609a0c3451 100644 --- a/src/ResourceStorage/Consumer/ConsumerFactory.php +++ b/src/ResourceStorage/Consumer/ConsumerFactory.php @@ -136,7 +136,7 @@ public function containerURI( StorableContainerResource $resource, SrcBuilder $src_builder, string $start_file = 'index.html', - float $valid_for_at_least_minutes = 60.0 + float $valid_for_at_least_minutes = 120.0 ): ContainerConsumer { return new ContainerURIConsumer( $src_builder, diff --git a/src/ResourceStorage/Consumer/ContainerURIConsumer.php b/src/ResourceStorage/Consumer/ContainerURIConsumer.php index c4e95f3bd8b4..b23f0ae01f8f 100755 --- a/src/ResourceStorage/Consumer/ContainerURIConsumer.php +++ b/src/ResourceStorage/Consumer/ContainerURIConsumer.php @@ -18,11 +18,10 @@ namespace ILIAS\ResourceStorage\Consumer; +use ILIAS\Filesystem\Util\Archive\Archives; use ILIAS\ResourceStorage\Resource\StorableResource; use ILIAS\ResourceStorage\Consumer\StreamAccess\StreamAccess; use ILIAS\ResourceStorage\Resource\StorableContainerResource; -use ILIAS\Filesystem\Util\Archive\Unzip; -use ILIAS\Filesystem\Util\Archive\UnzipOptions; use ILIAS\Data\URI; use ILIAS\FileDelivery\Delivery\StreamDelivery; @@ -33,10 +32,9 @@ class ContainerURIConsumer implements ContainerConsumer { use GetRevisionTrait; - private \ILIAS\Filesystem\Util\Archive\Archives $archives; + private Archives $archives; protected ?int $revision_number = null; private StorableResource $resource; - private StreamAccess $stream_access; /** * DownloadConsumer constructor. @@ -44,25 +42,33 @@ class ContainerURIConsumer implements ContainerConsumer public function __construct( private SrcBuilder $src_builder, StorableContainerResource $resource, - StreamAccess $stream_access, + private StreamAccess $stream_access, private string $start_file, private float $valid_for_at_least_minutes = 60.0 ) { global $DIC; $this->resource = $resource; $this->archives = $DIC->archives(); - $this->stream_access = $stream_access; } - public function getURI(): URI + public function getURI(): ?URI { + $filename = basename($this->start_file); + if ($filename === '') { + $filename = null; + } + $uri_string = $this->src_builder->getRevisionURL( $this->stream_access->populateRevision($this->getRevision()), true, - 60, - $this->valid_for_at_least_minutes - ) . StreamDelivery::SUBREQUEST_SEPARATOR . $this->start_file; + $this->valid_for_at_least_minutes, + $filename + ) . StreamDelivery::SUBREQUEST_SEPARATOR . urlencode($this->start_file); - return new URI($uri_string); + try { + return new URI($uri_string); + } catch (\Throwable) { + return null; + } } } diff --git a/src/ResourceStorage/Consumer/ContainerZIPAccessConsumer.php b/src/ResourceStorage/Consumer/ContainerZIPAccessConsumer.php index 24e1fea8e0e6..6b6fda1089ad 100644 --- a/src/ResourceStorage/Consumer/ContainerZIPAccessConsumer.php +++ b/src/ResourceStorage/Consumer/ContainerZIPAccessConsumer.php @@ -48,23 +48,12 @@ public function __construct(StorableContainerResource $resource, StreamAccess $s $this->stream_access = $stream_access; } - public function getZIP(): Unzip + public function getZIP(UnzipOptions $unzip_options = null): Unzip { $revision = $this->getRevision(); $revision = $this->stream_access->populateRevision($revision); - $zip_stream = $revision->maybeStreamResolver()?->getStream(); - $zip = new \ZipArchive(); - $zip->open($zip_stream->getMetadata()['uri'], \ZipArchive::RDONLY); - - $unzip_options = $this->archives - ->unzipOptions() - ->withDirectoryHandling(ZipDirectoryHandling::FLAT_STRUCTURE); - - return $this->archives->unzip( - $zip_stream, - $unzip_options - ); + return $this->archives->unzip($zip_stream, $unzip_options); } } diff --git a/src/ResourceStorage/Consumer/InlineSrcBuilder.php b/src/ResourceStorage/Consumer/InlineSrcBuilder.php index f64ca0bf3dc4..04905593708e 100644 --- a/src/ResourceStorage/Consumer/InlineSrcBuilder.php +++ b/src/ResourceStorage/Consumer/InlineSrcBuilder.php @@ -41,7 +41,8 @@ public function __construct( public function getRevisionURL( Revision $revision, bool $signed = true, - float $valid_for_at_least_minutes = 60.0 + float $valid_for_at_least_minutes = 60.0, + string $filename = null ): string { if ($signed) { throw new \RuntimeException('InlineSrcBuilder does not support signed URLs'); @@ -49,13 +50,13 @@ public function getRevisionURL( $sream_resolver = $revision->maybeStreamResolver(); if ($sream_resolver !== null) { $stream = $sream_resolver->getStream(); - if($sream_resolver->isInMemory()) { + if ($sream_resolver->isInMemory()) { return $this->buildDataURLFromStream($stream); } $this->file_delivery->buildTokenURL( $stream, - $revision->getTitle(), + $filename ?? $revision->getTitle(), Disposition::INLINE, 6, // FSX TODO 1 diff --git a/src/ResourceStorage/Consumer/SrcBuilder.php b/src/ResourceStorage/Consumer/SrcBuilder.php index 37130e1364e0..bfebac3fe7ef 100644 --- a/src/ResourceStorage/Consumer/SrcBuilder.php +++ b/src/ResourceStorage/Consumer/SrcBuilder.php @@ -29,9 +29,15 @@ interface SrcBuilder { /** + * @param string|null $filename * @throw \RuntimeException if signing is not possible or failed, but was requested with $signed = true */ - public function getRevisionURL(Revision $revision, bool $signed = true, float $valid_for_at_least_minutes = 60.0): string; + public function getRevisionURL( + Revision $revision, + bool $signed = true, + float $valid_for_at_least_minutes = 60.0, + string $filename = null + ): string; /** * @throw \RuntimeException if signing is not possible or failed, but was requested with $signed = true diff --git a/src/ResourceStorage/Consumer/SrcConsumer.php b/src/ResourceStorage/Consumer/SrcConsumer.php index 3e0b7c960df6..c6a7d576e88b 100644 --- a/src/ResourceStorage/Consumer/SrcConsumer.php +++ b/src/ResourceStorage/Consumer/SrcConsumer.php @@ -52,7 +52,8 @@ public function getSrc(bool $signed = false): string return $this->src_builder->getRevisionURL( $this->stream_access->populateRevision($this->getRevision()), $signed, - 60 + 60, + null ); } catch (\Throwable $e) { return ''; diff --git a/src/ResourceStorage/Manager/BaseManager.php b/src/ResourceStorage/Manager/BaseManager.php new file mode 100644 index 000000000000..e8ef48374a5a --- /dev/null +++ b/src/ResourceStorage/Manager/BaseManager.php @@ -0,0 +1,370 @@ + + */ +abstract class BaseManager +{ + protected ResourceBuilder $resource_builder; + protected CollectionBuilder $collection_builder; + protected RepositoryPreloader $preloader; + + /** + * Manager constructor. + */ + public function __construct( + ResourceBuilder $resource_builder, + CollectionBuilder $collection_builder, + RepositoryPreloader $preloader + ) { + $this->resource_builder = $resource_builder; + $this->collection_builder = $collection_builder; + $this->preloader = $preloader; + } + + /** + * @param bool|string $mimetype + * @return void + */ + protected function checkZIP(bool|string $mimetype): void + { + if (!in_array($mimetype, ['application/zip', 'application/x-zip-compressed'])) { + throw new \LogicException("Cant create container resource since stream is not a ZIP"); + } + } + + /** + * @description Publish a resource. A resource can contain a maximum of one revision in DRAFT on top status. + * This method can be used to publish this revision. If the latest revision is already published, nothing changes. + */ + public function publish(ResourceIdentification $rid): void + { + $this->resource_builder->publish($this->resource_builder->get($rid)); + } + + /** + * @description Unpublish a resource. The newest revision of a resource is set to the DRAFT status. + * If the latest revision is already in DRAFT, nothing changes. + */ + public function unpublish(ResourceIdentification $rid): void + { + $this->resource_builder->unpublish($this->resource_builder->get($rid)); + } + + protected function newStreamBased( + FileStream $stream, + ResourceStakeholder $stakeholder, + ResourceType $type, + string $revision_title = null + ): ResourceIdentification { + $info_resolver = new StreamInfoResolver( + $stream, + 1, + $stakeholder->getOwnerOfNewResources(), + $revision_title ?? $stream->getMetadata()['uri'] + ); + + $resource = $this->resource_builder->newFromStream( + $stream, + $info_resolver, + true, + $type + ); + $resource->addStakeholder($stakeholder); + $this->resource_builder->store($resource); + + return $resource->getIdentification(); + } + + public function find(string $identification): ?ResourceIdentification + { + $resource_identification = new ResourceIdentification($identification); + + if ($this->resource_builder->has($resource_identification)) { + return $resource_identification; + } + + return null; + } + + // Resources + + public function getResource(ResourceIdentification $i): StorableResource + { + $this->preloader->preload([$i->serialize()]); + return $this->resource_builder->get($i); + } + + public function remove(ResourceIdentification $identification, ResourceStakeholder $stakeholder): void + { + $this->resource_builder->remove($this->resource_builder->get($identification), $stakeholder); + if (!$this->resource_builder->has($identification)) { + $this->collection_builder->notififyResourceDeletion($identification); + } + } + + public function clone(ResourceIdentification $identification): ResourceIdentification + { + $resource = $this->resource_builder->clone($this->resource_builder->get($identification)); + + return $resource->getIdentification(); + } + + // Revision + + /** + * @description Append a new revision from an UploadResult. By passing $draft = true, the revision will be created as a + * DRAFT on top of the current revision. Consumers will always use the latest published revision. + * Appending new Revisions is not possible if the latest revision is already a DRAFT. In this case, + * the DRAFT will be updated. + */ + public function appendNewRevision( + ResourceIdentification $identification, + UploadResult $result, + ResourceStakeholder $stakeholder, + string $revision_title = null, + bool $draft = false + ): Revision { + if ($result->isOK()) { + if (!$this->resource_builder->has($identification)) { + throw new \LogicException( + "Resource not found, can't append new version in: " . $identification->serialize() + ); + } + $resource = $this->resource_builder->get($identification); + if ($resource->getType() === ResourceType::CONTAINER) { + $this->checkZIP($result->getMimeType()); + } + + $info_resolver = new UploadInfoResolver( + $result, + $resource->getMaxRevision(true) + 1, + $stakeholder->getOwnerOfNewResources(), + $revision_title ?? $result->getName() + ); + + $this->resource_builder->append( + $resource, + $result, + $info_resolver, + $draft ? RevisionStatus::DRAFT : RevisionStatus::PUBLISHED + ); + $resource->addStakeholder($stakeholder); + + $this->resource_builder->store($resource); + + return $resource->getCurrentRevisionIncludingDraft(); + } + throw new \LogicException("Can't handle UploadResult: " . $result->getStatus()->getMessage()); + } + + /** + * @throws \ILIAS\ResourceStorage\Policy\FileNamePolicyException if the filename is not allowed + * @throws \LogicException if the resource is not found + * @throws \LogicException if the resource is a container and the stream is not a ZIP + * @throws \LogicException if the latest revision is a DRAFT + */ + public function replaceWithUpload( + ResourceIdentification $identification, + UploadResult $result, + ResourceStakeholder $stakeholder, + string $revision_title = null + ): Revision { + if ($result->isOK()) { + if (!$this->resource_builder->has($identification)) { + throw new \LogicException( + "Resource not found, can't append new version in: " . $identification->serialize() + ); + } + $resource = $this->resource_builder->get($identification); + if ($resource->getType() === ResourceType::CONTAINER) { + $this->checkZIP($result->getMimeType()); + } + if ($resource->getCurrentRevisionIncludingDraft()->getStatus() === RevisionStatus::DRAFT) { + throw new \LogicException( + "Can't replace DRAFT revision, use appendNewRevision instead to update the DRAFT" + ); + } + $info_resolver = new UploadInfoResolver( + $result, + $resource->getMaxRevision(true) + 1, + $stakeholder->getOwnerOfNewResources(), + $revision_title ?? $result->getName() + ); + $this->resource_builder->replaceWithUpload( + $resource, + $result, + $info_resolver + ); + $resource->addStakeholder($stakeholder); + + $this->resource_builder->store($resource); + + return $resource->getCurrentRevisionIncludingDraft(); + } + throw new \LogicException("Can't handle UploadResult: " . $result->getStatus()->getMessage()); + } + + /** + * @description Append a new revision from a stream. By passing $draft = true, the revision will be created as a + * DRAFT on top of the current revision. Consumers will always use the latest published revision. + * Appending new Revisions is not possible if the latest revision is already a DRAFT. In this case, + * the DRAFT will be updated. + */ + public function appendNewRevisionFromStream( + ResourceIdentification $identification, + FileStream $stream, + ResourceStakeholder $stakeholder, + string $revision_title = null, + bool $draft = false + ): Revision { + if (!$this->resource_builder->has($identification)) { + throw new \LogicException( + "Resource not found, can't append new version in: " . $identification->serialize() + ); + } + + $resource = $this->resource_builder->get($identification); + if ($resource->getType() === ResourceType::CONTAINER) { + $this->checkZIP(mime_content_type($stream->getMetadata()['uri'])); + } + $info_resolver = new StreamInfoResolver( + $stream, + $resource->getMaxRevision(true) + 1, + $stakeholder->getOwnerOfNewResources(), + $revision_title ?? $stream->getMetadata()['uri'] + ); + + $this->resource_builder->appendFromStream( + $resource, + $stream, + $info_resolver, + $draft ? RevisionStatus::DRAFT : RevisionStatus::PUBLISHED, + true + ); + + $resource->addStakeholder($stakeholder); + + $this->resource_builder->store($resource); + + return $resource->getCurrentRevisionIncludingDraft(); + } + + /** + * @throws \ILIAS\ResourceStorage\Policy\FileNamePolicyException if the filename is not allowed + * @throws \LogicException if the resource is not found + * @throws \LogicException if the resource is a container and the stream is not a ZIP + * @throws \LogicException if the latest revision is a DRAFT + */ + public function replaceWithStream( + ResourceIdentification $identification, + FileStream $stream, + ResourceStakeholder $stakeholder, + string $revision_title = null + ): Revision { + if (!$this->resource_builder->has($identification)) { + throw new \LogicException( + "Resource not found, can't append new version in: " . $identification->serialize() + ); + } + + $resource = $this->resource_builder->get($identification); + if ($resource->getCurrentRevisionIncludingDraft()->getStatus() === RevisionStatus::DRAFT) { + throw new \LogicException( + "Can't replace DRAFT revision, use appendNewRevisionFromStream instead to update the DRAFT" + ); + } + if ($resource->getType() === ResourceType::CONTAINER) { + $this->checkZIP(mime_content_type($stream->getMetadata()['uri'])); + } + $info_resolver = new StreamInfoResolver( + $stream, + $resource->getMaxRevision(true) + 1, + $stakeholder->getOwnerOfNewResources(), + $revision_title ?? $stream->getMetadata()['uri'] + ); + + $this->resource_builder->replaceWithStream( + $resource, + $stream, + $info_resolver, + true + ); + $resource->addStakeholder($stakeholder); + + $this->resource_builder->store($resource); + + return $resource->getCurrentRevisionIncludingDraft(); + } + + public function getCurrentRevision( + ResourceIdentification $identification + ): Revision { + return $this->resource_builder->get($identification)->getCurrentRevision(); + } + + public function getCurrentRevisionIncludingDraft( + ResourceIdentification $identification + ): Revision { + return $this->resource_builder->get($identification)->getCurrentRevisionIncludingDraft(); + } + + public function updateRevision(Revision $revision): bool + { + $this->resource_builder->storeRevision($revision); + + return true; + } + + public function rollbackRevision(ResourceIdentification $identification, int $revision_number): bool + { + $resource = $this->resource_builder->get($identification); + $this->resource_builder->appendFromRevision($resource, $revision_number); + $this->resource_builder->store($resource); + + return true; + } + + public function removeRevision(ResourceIdentification $identification, int $revision_number): bool + { + $resource = $this->resource_builder->get($identification); + $this->resource_builder->removeRevision($resource, $revision_number); + $this->resource_builder->store($resource); + + return true; + } +} diff --git a/src/ResourceStorage/Manager/ContainerManager.php b/src/ResourceStorage/Manager/ContainerManager.php new file mode 100644 index 000000000000..c47ad5c6182d --- /dev/null +++ b/src/ResourceStorage/Manager/ContainerManager.php @@ -0,0 +1,128 @@ + + */ +final class ContainerManager extends BaseManager +{ + protected function normalizePath(string $path_inside_container): string + { + $path_inside_container = '/' . ltrim($path_inside_container, './'); + $path_inside_container = rtrim($path_inside_container, '/'); + + return $path_inside_container; + } + + public function containerFromUpload( + UploadResult $result, + ResourceStakeholder $stakeholder, + string $revision_title = null + ): ResourceIdentification { + // check if stream is a ZIP + $this->checkZIP(mime_content_type($result->getMimeType())); + + return $this->upload($result, $stakeholder, $revision_title); + } + + public function containerFromStream( + FileStream $stream, + ResourceStakeholder $stakeholder, + string $revision_title = null + ): ResourceIdentification { + // check if stream is a ZIP + $this->checkZIP(mime_content_type($stream->getMetadata()['uri'])); + + return $this->newStreamBased( + $stream, + $stakeholder, + ResourceType::CONTAINER, + $revision_title + ); + } + + public function createDirectoryInsideContainer( + ResourceIdentification $container, + string $path_inside_container + ): bool { + $path_inside_container = $this->normalizePath($path_inside_container); + if (empty($path_inside_container)) { + return false; + } + return $this->resource_builder->createDirectoryInsideContainer( + $this->getResource($container), + $path_inside_container + ); + } + + public function removePathInsideContainer( + ResourceIdentification $container, + string $path_inside_container + ): bool { + if (empty($path_inside_container)) { + return false; + } + return $this->resource_builder->removePathInsideContainer( + $this->getResource($container), + $path_inside_container + ); + } + + public function addUploadToContainer( + ResourceIdentification $container, + UploadResult $result, + string $parent_path_inside_container, + ): bool { + $parent_path_inside_container = $this->normalizePath($parent_path_inside_container); + if (empty($parent_path_inside_container)) { + $parent_path_inside_container = '/'; + } + return $this->resource_builder->addUploadToContainer( + $this->getResource($container), + $result, + $parent_path_inside_container + ); + } + + public function addStreamToContainer( + ResourceIdentification $container, + FileStream $stream, + string $path_inside_container, + ): bool { + $path_inside_container = $this->normalizePath($path_inside_container); + if (empty($path_inside_container)) { + return false; + } + return $this->resource_builder->addStreamToContainer( + $this->getResource($container), + $stream, + $path_inside_container + ); + } + +} diff --git a/src/ResourceStorage/Manager/Manager.php b/src/ResourceStorage/Manager/Manager.php index 686c2c41f732..75eda56d2774 100644 --- a/src/ResourceStorage/Manager/Manager.php +++ b/src/ResourceStorage/Manager/Manager.php @@ -35,57 +35,10 @@ use ILIAS\ResourceStorage\Revision\RevisionStatus; /** - * Class StorageManager * @author Fabian Schmid */ -class Manager +class Manager extends BaseManager { - protected ResourceBuilder $resource_builder; - protected CollectionBuilder $collection_builder; - protected RepositoryPreloader $preloader; - - /** - * Manager constructor. - */ - public function __construct( - ResourceBuilder $resource_builder, - CollectionBuilder $collection_builder, - RepositoryPreloader $preloader - ) { - $this->resource_builder = $resource_builder; - $this->collection_builder = $collection_builder; - $this->preloader = $preloader; - } - - /** - * @param bool|string $mimetype - * @return void - */ - protected function checkZIP(bool|string $mimetype): void - { - if (!in_array($mimetype, ['application/zip', 'application/x-zip-compressed'])) { - throw new \LogicException("Cant create container resource since stream is not a ZIP"); - } - } - - /** - * @description Publish a resource. A resource can contain a maximum of one revision in DRAFT on top status. - * This method can be used to publish this revision. If the latest revision is already published, nothing changes. - */ - public function publish(ResourceIdentification $rid): void - { - $this->resource_builder->publish($this->resource_builder->get($rid)); - } - - /** - * @description Unpublish a resource. The newest revision of a resource is set to the DRAFT status. - * If the latest revision is already in DRAFT, nothing changes. - */ - public function unpublish(ResourceIdentification $rid): void - { - $this->resource_builder->unpublish($this->resource_builder->get($rid)); - } - /** * @description Creates a new resource from an upload, the status in this case is always PUBLISHED. */ @@ -114,17 +67,6 @@ public function upload( throw new \LogicException("Can't handle UploadResult: " . $result->getStatus()->getMessage()); } - public function containerFromUpload( - UploadResult $result, - ResourceStakeholder $stakeholder, - string $revision_title = null - ): ResourceIdentification { - // check if stream is a ZIP - $this->checkZIP(mime_content_type($result->getMimeType())); - - return $this->upload($result, $stakeholder, $revision_title); - } - public function stream( FileStream $stream, ResourceStakeholder $stakeholder, @@ -138,299 +80,4 @@ public function stream( ); } - private function newStreamBased( - FileStream $stream, - ResourceStakeholder $stakeholder, - ResourceType $type, - string $revision_title = null - ): ResourceIdentification - { - $info_resolver = new StreamInfoResolver( - $stream, - 1, - $stakeholder->getOwnerOfNewResources(), - $revision_title ?? $stream->getMetadata()['uri'] - ); - - $resource = $this->resource_builder->newFromStream( - $stream, - $info_resolver, - true, - $type - ); - $resource->addStakeholder($stakeholder); - $this->resource_builder->store($resource); - - return $resource->getIdentification(); - } - - public function containerFromStream( - FileStream $stream, - ResourceStakeholder $stakeholder, - string $revision_title = null - ): ResourceIdentification { - // check if stream is a ZIP - $this->checkZIP(mime_content_type($stream->getMetadata()['uri'])); - - return $this->newStreamBased( - $stream, - $stakeholder, - ResourceType::CONTAINER, - $revision_title - ); - } - - public function find(string $identification): ?ResourceIdentification - { - $resource_identification = new ResourceIdentification($identification); - - if ($this->resource_builder->has($resource_identification)) { - return $resource_identification; - } - - return null; - } - - // Resources - - public function getResource(ResourceIdentification $i): StorableResource - { - $this->preloader->preload([$i->serialize()]); - return $this->resource_builder->get($i); - } - - - public function remove(ResourceIdentification $identification, ResourceStakeholder $stakeholder): void - { - $this->resource_builder->remove($this->resource_builder->get($identification), $stakeholder); - if (!$this->resource_builder->has($identification)) { - $this->collection_builder->notififyResourceDeletion($identification); - } - } - - public function clone(ResourceIdentification $identification): ResourceIdentification - { - $resource = $this->resource_builder->clone($this->resource_builder->get($identification)); - - return $resource->getIdentification(); - } - - // Revision - - /** - * @description Append a new revision from an UploadResult. By passing $draft = true, the revision will be created as a - * DRAFT on top of the current revision. Consumers will always use the latest published revision. - * Appending new Revisions is not possible if the latest revision is already a DRAFT. In this case, - * the DRAFT will be updated. - */ - public function appendNewRevision( - ResourceIdentification $identification, - UploadResult $result, - ResourceStakeholder $stakeholder, - string $revision_title = null, - bool $draft = false - ): Revision { - if ($result->isOK()) { - if (!$this->resource_builder->has($identification)) { - throw new \LogicException( - "Resource not found, can't append new version in: " . $identification->serialize() - ); - } - $resource = $this->resource_builder->get($identification); - if ($resource->getType() === ResourceType::CONTAINER) { - $this->checkZIP($result->getMimeType()); - } - - $info_resolver = new UploadInfoResolver( - $result, - $resource->getMaxRevision(true) + 1, - $stakeholder->getOwnerOfNewResources(), - $revision_title ?? $result->getName() - ); - - $this->resource_builder->append( - $resource, - $result, - $info_resolver, - $draft ? RevisionStatus::DRAFT : RevisionStatus::PUBLISHED - ); - $resource->addStakeholder($stakeholder); - - $this->resource_builder->store($resource); - - return $resource->getCurrentRevisionIncludingDraft(); - } - throw new \LogicException("Can't handle UploadResult: " . $result->getStatus()->getMessage()); - } - - /** - * @throws \ILIAS\ResourceStorage\Policy\FileNamePolicyException if the filename is not allowed - * @throws \LogicException if the resource is not found - * @throws \LogicException if the resource is a container and the stream is not a ZIP - * @throws \LogicException if the latest revision is a DRAFT - */ - public function replaceWithUpload( - ResourceIdentification $identification, - UploadResult $result, - ResourceStakeholder $stakeholder, - string $revision_title = null - ): Revision { - if ($result->isOK()) { - if (!$this->resource_builder->has($identification)) { - throw new \LogicException( - "Resource not found, can't append new version in: " . $identification->serialize() - ); - } - $resource = $this->resource_builder->get($identification); - if ($resource->getType() === ResourceType::CONTAINER) { - $this->checkZIP($result->getMimeType()); - } - if ($resource->getCurrentRevisionIncludingDraft()->getStatus() === RevisionStatus::DRAFT) { - throw new \LogicException("Can't replace DRAFT revision, use appendNewRevision instead to update the DRAFT"); - } - $info_resolver = new UploadInfoResolver( - $result, - $resource->getMaxRevision(true) + 1, - $stakeholder->getOwnerOfNewResources(), - $revision_title ?? $result->getName() - ); - $this->resource_builder->replaceWithUpload( - $resource, - $result, - $info_resolver - ); - $resource->addStakeholder($stakeholder); - - $this->resource_builder->store($resource); - - return $resource->getCurrentRevisionIncludingDraft(); - } - throw new \LogicException("Can't handle UploadResult: " . $result->getStatus()->getMessage()); - } - - /** - * @description Append a new revision from a stream. By passing $draft = true, the revision will be created as a - * DRAFT on top of the current revision. Consumers will always use the latest published revision. - * Appending new Revisions is not possible if the latest revision is already a DRAFT. In this case, - * the DRAFT will be updated. - */ - public function appendNewRevisionFromStream( - ResourceIdentification $identification, - FileStream $stream, - ResourceStakeholder $stakeholder, - string $revision_title = null, - bool $draft = false - ): Revision { - if (!$this->resource_builder->has($identification)) { - throw new \LogicException( - "Resource not found, can't append new version in: " . $identification->serialize() - ); - } - - $resource = $this->resource_builder->get($identification); - if ($resource->getType() === ResourceType::CONTAINER) { - $this->checkZIP(mime_content_type($stream->getMetadata()['uri'])); - } - $info_resolver = new StreamInfoResolver( - $stream, - $resource->getMaxRevision(true) + 1, - $stakeholder->getOwnerOfNewResources(), - $revision_title ?? $stream->getMetadata()['uri'] - ); - - $this->resource_builder->appendFromStream( - $resource, - $stream, - $info_resolver, - $draft ? RevisionStatus::DRAFT : RevisionStatus::PUBLISHED, - true - ); - - $resource->addStakeholder($stakeholder); - - $this->resource_builder->store($resource); - - return $resource->getCurrentRevisionIncludingDraft(); - } - - /** - * @throws \ILIAS\ResourceStorage\Policy\FileNamePolicyException if the filename is not allowed - * @throws \LogicException if the resource is not found - * @throws \LogicException if the resource is a container and the stream is not a ZIP - * @throws \LogicException if the latest revision is a DRAFT - */ - public function replaceWithStream( - ResourceIdentification $identification, - FileStream $stream, - ResourceStakeholder $stakeholder, - string $revision_title = null - ): Revision { - if (!$this->resource_builder->has($identification)) { - throw new \LogicException( - "Resource not found, can't append new version in: " . $identification->serialize() - ); - } - - $resource = $this->resource_builder->get($identification); - if ($resource->getCurrentRevisionIncludingDraft()->getStatus() === RevisionStatus::DRAFT) { - throw new \LogicException("Can't replace DRAFT revision, use appendNewRevisionFromStream instead to update the DRAFT"); - } - if ($resource->getType() === ResourceType::CONTAINER) { - $this->checkZIP(mime_content_type($stream->getMetadata()['uri'])); - } - $info_resolver = new StreamInfoResolver( - $stream, - $resource->getMaxRevision(true) + 1, - $stakeholder->getOwnerOfNewResources(), - $revision_title ?? $stream->getMetadata()['uri'] - ); - - $this->resource_builder->replaceWithStream( - $resource, - $stream, - $info_resolver, - true - ); - $resource->addStakeholder($stakeholder); - - $this->resource_builder->store($resource); - - return $resource->getCurrentRevisionIncludingDraft(); - } - - public function getCurrentRevision( - ResourceIdentification $identification - ): Revision { - return $this->resource_builder->get($identification)->getCurrentRevision(); - } - public function getCurrentRevisionIncludingDraft( - ResourceIdentification $identification - ): Revision { - return $this->resource_builder->get($identification)->getCurrentRevisionIncludingDraft(); - } - - public function updateRevision(Revision $revision): bool - { - $this->resource_builder->storeRevision($revision); - - return true; - } - - public function rollbackRevision(ResourceIdentification $identification, int $revision_number): bool - { - $resource = $this->resource_builder->get($identification); - $this->resource_builder->appendFromRevision($resource, $revision_number); - $this->resource_builder->store($resource); - - return true; - } - - public function removeRevision(ResourceIdentification $identification, int $revision_number): bool - { - $resource = $this->resource_builder->get($identification); - $this->resource_builder->removeRevision($resource, $revision_number); - $this->resource_builder->store($resource); - - return true; - } } diff --git a/src/ResourceStorage/Resource/ResourceBuilder.php b/src/ResourceStorage/Resource/ResourceBuilder.php index d791977f267f..c3f99f8adc42 100644 --- a/src/ResourceStorage/Resource/ResourceBuilder.php +++ b/src/ResourceStorage/Resource/ResourceBuilder.php @@ -199,7 +199,12 @@ public function replaceWithUpload( if ($resource->getCurrentRevisionIncludingDraft()->getStatus() === RevisionStatus::DRAFT) { throw new \LogicException('You can not replace a draft revision, you must publish it first'); } - $revision = $this->revision_repository->blankFromUpload($info_resolver, $resource, $result, RevisionStatus::PUBLISHED); + $revision = $this->revision_repository->blankFromUpload( + $info_resolver, + $resource, + $result, + RevisionStatus::PUBLISHED + ); $revision = $this->populateRevisionInfo($revision, $info_resolver); foreach ($resource->getAllRevisionsIncludingDraft() as $existing_revision) { @@ -229,7 +234,13 @@ public function appendFromStream( ); } - $new_revision = $this->revision_repository->blankFromStream($info_resolver, $resource, $stream, $status, $keep_original); + $new_revision = $this->revision_repository->blankFromStream( + $info_resolver, + $resource, + $stream, + $status, + $keep_original + ); if ($resource->getCurrentRevisionIncludingDraft()->getStatus() === RevisionStatus::DRAFT) { $clone_revision = $this->buildDraftReplacementRevision($resource, $new_revision, $info_resolver); @@ -541,6 +552,179 @@ public function removeRevision(StorableResource $resource, int $revision_number) $this->store($resource); } + // Container Actions + public function createDirectoryInsideContainer( + StorableContainerResource $container, + string $path_inside_container, + ): bool { + $revision = $container->getCurrentRevisionIncludingDraft(); + $stream = $this->extractStream($revision); + + // create directory inside ZipArchive + try { + $zip = new \ZipArchive(); + $zip->open($stream->getMetadata()['uri']); + $path_inside_container = $this->ensurePathInZIP($zip, $path_inside_container, false); + $zip->close(); + + // cleanup revision and flavours + $this->storage_handler_factory->getHandlerForRevision($revision)->clearFlavours($revision); + $revision->getInformation()->setSize(filesize($stream->getMetadata()['uri'])); + $this->storeRevision($revision); + + return true; + } catch (\Throwable $exception) { + return false; + } + } + + private function ensurePathInZIP(\ZipArchive $zip, string $path, bool $is_file): string + { + if ($path === '' || $path === '/') { + return $path; + } + + $filename = ''; + if ($is_file) { + $filename = basename($path); + $path = dirname($path); + } + + // try to determine if a path inside the zip exists with or without a slash at the beginning + // determine root directory of the path using regex + $parts = explode('/', $path); + $root = array_shift($parts); + $root = $root === '' ? array_shift($parts) : $root; + + // check if the root directory exists without a slash at the beginning + if ($zip->locateName($root . '/') !== false) { + $root = $root; + } elseif ($zip->locateName('/' . $root . '/') !== false) { + // check if the root directory exists with a slash at the beginning + $root = '/' . $root; + } else { + // if the root directory does not exist, create it + $zip->addEmptyDir($root); + } + + $path_inside_container = $root; + foreach ($parts as $part) { + $path_inside_container .= '/' . $part; + if ($zip->locateName($path_inside_container . '/') === false) { + $zip->addEmptyDir($path_inside_container . '/'); + } + } + + return rtrim($path_inside_container, '/') . '/' . $filename; + } + + public function removePathInsideContainer( + StorableContainerResource $container, + string $path_inside_container, + ): bool { + $revision = $container->getCurrentRevisionIncludingDraft(); + $stream = $this->extractStream($revision); + + // create directory inside ZipArchive + try { + $zip = new \ZipArchive(); + $zip->open($stream->getMetadata()['uri']); + + $return = $zip->deleteName($path_inside_container); + // remove all files inside the directory + for ($i = 0; $i < $zip->numFiles; $i++) { + $path = $zip->getNameIndex($i); + if ($path === false) { + continue; + } + if (strpos($path, $path_inside_container) === 0) { + $zip->deleteIndex($i); + } + } + + $zip->close(); + + // cleanup revision and flavours + $this->storage_handler_factory->getHandlerForRevision($revision)->clearFlavours($revision); + $revision->getInformation()->setSize(filesize($stream->getMetadata()['uri'])); + $this->storeRevision($revision); + + return $return; + } catch (\Throwable $exception) { + $this->storage_handler_factory->getHandlerForRevision($revision)->clearFlavours($revision); + return false; + } + } + + public function addUploadToContainer( + StorableContainerResource $container, + UploadResult $result, + string $parent_path_inside_container, + ): bool { + $revision = $container->getCurrentRevisionIncludingDraft(); + $stream = $this->extractStream($revision); + + // create directory inside ZipArchive + try { + $zip = new \ZipArchive(); + $zip->open($stream->getMetadata()['uri']); + + $parent_path_inside_container = $this->ensurePathInZIP($zip, $parent_path_inside_container, false); + + $path_inside_zip = rtrim($parent_path_inside_container, '/') . '/' . $result->getName(); + + $return = $zip->addFile( + $result->getPath(), + $path_inside_zip + ); + $zip->close(); + + // cleanup revision and flavours + $this->storage_handler_factory->getHandlerForRevision($revision)->clearFlavours($revision); + $revision->getInformation()->setSize(filesize($stream->getMetadata()['uri'])); + $this->storeRevision($revision); + + return $return; + } catch (\Throwable $exception) { + return false; + } + + return true; + } + + public function addStreamToContainer( + StorableContainerResource $container, + FileStream $stream, + string $path_inside_container, + ): bool { + $revision = $container->getCurrentRevisionIncludingDraft(); + $revision_stream = $this->extractStream($revision); + + try { + $zip = new \ZipArchive(); + $zip->open($revision_stream->getMetadata()['uri']); + + $path_inside_container = $this->ensurePathInZIP($zip, $path_inside_container, true); + + $return = $zip->addFromString( + $path_inside_container, + (string) $stream + ); + $zip->close(); + + // cleanup revision and flavours + $this->storage_handler_factory->getHandlerForRevision($revision)->clearFlavours($revision); + $revision->getInformation()->setSize(filesize($revision_stream->getMetadata()['uri'])); + $this->storeRevision($revision); + + return $return; + } catch (\Throwable $exception) { + return false; + } + + return true; + } + private function deleteRevision(StorableResource $resource, Revision $revision): void { try { diff --git a/src/ResourceStorage/Services.php b/src/ResourceStorage/Services.php index ee2d4d1e8be9..f945d2ce0533 100644 --- a/src/ResourceStorage/Services.php +++ b/src/ResourceStorage/Services.php @@ -42,6 +42,7 @@ use ILIAS\ResourceStorage\StorageHandler\StorageHandler; use ILIAS\ResourceStorage\StorageHandler\StorageHandlerFactory; use ILIAS\ResourceStorage\Events\Subject; +use ILIAS\ResourceStorage\Manager\ContainerManager; /** * Class Services @@ -52,6 +53,7 @@ class Services { protected Subject $events; protected \ILIAS\ResourceStorage\Manager\Manager $manager; + protected ContainerManager $container_manager; protected \ILIAS\ResourceStorage\Consumer\Consumers $consumers; protected \ILIAS\ResourceStorage\Collection\Collections $collections; protected \ILIAS\ResourceStorage\Flavour\Flavours $flavours; @@ -94,6 +96,12 @@ public function __construct( $collection_builder, $this->preloader ); + $this->container_manager = new ContainerManager( + $resource_builder, + $collection_builder, + $this->preloader, + $this->events + ); $this->consumers = new Consumers( new ConsumerFactory( $stream_access, @@ -129,6 +137,11 @@ public function manage(): Manager return $this->manager; } + public function manageContainer(): ContainerManager + { + return $this->container_manager; + } + public function consume(): Consumers { return $this->consumers; diff --git a/src/ResourceStorage/StorageHandler/FileSystemBased/AbstractFileSystemStorageHandler.php b/src/ResourceStorage/StorageHandler/FileSystemBased/AbstractFileSystemStorageHandler.php index e98275aa0ac6..dac8b1135127 100644 --- a/src/ResourceStorage/StorageHandler/FileSystemBased/AbstractFileSystemStorageHandler.php +++ b/src/ResourceStorage/StorageHandler/FileSystemBased/AbstractFileSystemStorageHandler.php @@ -314,6 +314,11 @@ public function getFlavourPath(Revision $revision, Flavour $flavour): string . '/' . $flavour->getPersistingName(); } + public function clearFlavours(Revision $revision): void + { + $this->fs->deleteDir($this->getRevisionPath($revision) . '/' . self::FLAVOUR_PATH_PREFIX); + } + public function getRevisionPath(Revision $revision): string { return $this->getFullContainerPath($revision->getIdentification()) . '/' . $revision->getVersionNumber(); diff --git a/src/ResourceStorage/StorageHandler/StorageHandler.php b/src/ResourceStorage/StorageHandler/StorageHandler.php index 67e87d9306ef..f569ae6171e8 100644 --- a/src/ResourceStorage/StorageHandler/StorageHandler.php +++ b/src/ResourceStorage/StorageHandler/StorageHandler.php @@ -73,6 +73,8 @@ public function getFlavourStreams(Revision $revision, Flavour $flavour): \Genera public function getFlavourPath(Revision $revision, Flavour $flavour): string; + public function clearFlavours(Revision $revision): void; + // REVISIONS public function cloneRevision(CloneRevision $revision): bool; diff --git a/tests/Filesystem/Util/Convert/ImageConversionTest.php b/tests/Filesystem/Util/Convert/ImageConversionTest.php index a7c90ca56ee7..cab1b3b27607 100644 --- a/tests/Filesystem/Util/Convert/ImageConversionTest.php +++ b/tests/Filesystem/Util/Convert/ImageConversionTest.php @@ -72,7 +72,7 @@ public function testImageThumbnailActualImage(): void $this->assertNull($thumbnail_converter->getThrowableIfAny()); $converted_stream = $thumbnail_converter->getStream(); - $getimagesizefromstring = getimagesizefromstring((string)$converted_stream); + $getimagesizefromstring = getimagesizefromstring((string) $converted_stream); $this->assertEquals(75, $getimagesizefromstring[0]); // width $this->assertEquals(100, $getimagesizefromstring[1]); // height @@ -104,7 +104,7 @@ public function testImageSquareActualImage(): void $this->assertEquals(200, $getimagesizefromstring[self::H]); } - public function getImageSizesByWidth(): array + public static function getImageSizesByWidth(): array { return [ [400, 300, self::BY_WIDTH_FINAL, 192], @@ -155,7 +155,7 @@ public function testResizeToFitWidth( ); } - public function getImageSizesByHeight(): array + public static function getImageSizesByHeight(): array { return [ [400, 300, self::BY_HEIGHT_FINAL, 1008], @@ -207,7 +207,7 @@ public function testResizeToFitHeight( ); } - public function getImageSizesByFixed(): array + public static function getImageSizesByFixed(): array { return [ [1024, 768, 300, 100, true], @@ -242,7 +242,7 @@ public function testResizeByFixedSize( $this->assertEquals($final_height, $new_dimensions[self::H]); } - public function getImageOptions(): array + public static function getImageOptions(): array { $options = new ImageOutputOptions(); return [ @@ -307,7 +307,7 @@ public function testImageOutputOptionSanity(): void $this->assertEquals(75, $options->getQuality()); // original options should not change } - public function getWrongFormats(): array + public static function getWrongFormats(): array { return [ ['gif'], @@ -326,7 +326,7 @@ public function testWrongFormats(string $format): void $wrong = $options->withFormat($format); } - public function getWrongQualites(): array + public static function getWrongQualites(): array { return [ [-1], @@ -390,7 +390,7 @@ public function testFailed(): void $this->assertInstanceOf(\Throwable::class, $resized->getThrowableIfAny()); } - public function getColors(): array + public static function getColors(): array { return [ [null], @@ -444,7 +444,7 @@ public function testBackgroundColor(?string $color): void $converter = new ImageConverter($converter_options, $output_options, $png); $this->assertTrue($converter->isOK()); $converted_stream = $converter->getStream(); - $gd_image = imagecreatefromstring((string)$converted_stream); + $gd_image = imagecreatefromstring((string) $converted_stream); $colors = imagecolorsforindex($gd_image, imagecolorat($gd_image, 1, 1)); $color_in_converted_picture = sprintf("#%02x%02x%02x", $colors['red'], $colors['green'], $colors['blue']); @@ -478,6 +478,34 @@ public function testWriteImage(): void unlink($output_path); } + public function testHighDensityPixel(): void + { + $file = 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Jan_Vermeer_-_The_Art_of_Painting_-_Google_Art_Project.jpg'; + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, $file); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_USERAGENT, 'PHPUnit/1.0'); + $string = curl_exec($curl); + curl_close($curl); + + $img = Streams::ofString($string); + $this->assertInstanceOf(FileStream::class, $img); + + $converter_options = (new ImageConversionOptions()) + ->withWidth(80) + ->withHeight(80) + ->withKeepAspectRatio(true) + ->withCrop(true) + ->withThrowOnError(true); + + $output_options = (new ImageOutputOptions()) + ->withQuality(60) + ->withFormat(ImageOutputOptions::FORMAT_PNG); + + $converter = new ImageConverter($converter_options, $output_options, $img); + $this->assertTrue($converter->isOK()); + } + protected function checkImagick(): void { @@ -488,10 +516,10 @@ protected function checkImagick(): void protected function getImageSizeFromStream(FileStream $stream): array { - $getimagesizefromstring = getimagesizefromstring((string)$stream); + $getimagesizefromstring = getimagesizefromstring((string) $stream); return [ - self::W => (int)round($getimagesizefromstring[0]), - self::H => (int)round($getimagesizefromstring[1]) + self::W => (int) round($getimagesizefromstring[0]), + self::H => (int) round($getimagesizefromstring[1]) ]; } @@ -504,7 +532,7 @@ protected function getImageQualityFromStream(FileStream $stream): int { $stream->rewind(); $img = new \Imagick(); - $img->readImageBlob((string)$stream); + $img->readImageBlob((string) $stream); return $img->getImageCompressionQuality(); } diff --git a/tests/Filesystem/Util/Convert/LegacyImageConversionTest.php b/tests/Filesystem/Util/Convert/LegacyImageConversionTest.php index 3f8b009ef64d..ab6a7fc56278 100644 --- a/tests/Filesystem/Util/Convert/LegacyImageConversionTest.php +++ b/tests/Filesystem/Util/Convert/LegacyImageConversionTest.php @@ -40,7 +40,7 @@ protected function setUp(): void } - public function someDefinitions(): array + public static function someDefinitions(): array { return [ [100, 100, 'jpg', 'image/jpeg'], @@ -87,7 +87,7 @@ public function testImageThumbnailActualImage( $this->assertEquals($expected_quality, $test_image->getImageCompressionQuality()); $this->assertEquals($expected_height, $test_image->getImageHeight()); - $this->assertEquals((int)round($expected_height * 0.75), $test_image->getImageWidth()); + $this->assertEquals((int) round($expected_height * 0.75), $test_image->getImageWidth()); unlink($temp_file); } diff --git a/tests/Filesystem/Util/FilenameSanitizing.php b/tests/Filesystem/Util/FilenameSanitizing.php index 99ea53479038..784d0747eb51 100644 --- a/tests/Filesystem/Util/FilenameSanitizing.php +++ b/tests/Filesystem/Util/FilenameSanitizing.php @@ -26,7 +26,7 @@ */ class FilenameSanitizing extends TestCase { - public function provideFilenames(): array + public static function provideFilenames(): array { return [ ["Control\u{00a0}Character", 'ControlCharacter'], diff --git a/tests/Filesystem/Util/UnzipTest.php b/tests/Filesystem/Util/UnzipTest.php index bb52b6815876..4fe372174650 100644 --- a/tests/Filesystem/Util/UnzipTest.php +++ b/tests/Filesystem/Util/UnzipTest.php @@ -151,7 +151,7 @@ public function testEnsureTopDirectory(): void $unzipped_files = $this->directoryToArray($temp_unzip_path); - $this->assertSame($this->top_directory_tree, $unzipped_files); + $this->assertSame(self::$top_directory_tree, $unzipped_files); $this->assertTrue($this->recurseRmdir($temp_unzip_path)); } @@ -174,7 +174,7 @@ public function testFlatLegacyUnzip(): void $unzipped_files = $this->directoryToArray($temp_unzip_path); - $this->assertSame($this->expected_flat_files, $unzipped_files); + $this->assertSame(self::$expected_flat_files, $unzipped_files); $this->assertTrue($this->recurseRmdir($temp_unzip_path)); } @@ -212,18 +212,18 @@ private function directoryToArray(string $path_to_directory): array // PROVIDERS - public function getZips(): array + public static function getZips(): array { return [ - ['1_folder_mac.zip', false, 10, $this->directories_one, 15, $this->files_one], - ['1_folder_win.zip', false, 10, $this->directories_one, 15, $this->files_one], - ['3_folders_mac.zip', true, 9, $this->directories_three, 12, $this->files_three], - ['3_folders_win.zip', true, 9, $this->directories_three, 12, $this->files_three], - ['1_folder_1_file_mac.zip', true, 3, $this->directories_mixed, 5, $this->files_mixed] + ['1_folder_mac.zip', false, 10, self::$directories_one, 15, self::$files_one], + ['1_folder_win.zip', false, 10, self::$directories_one, 15, self::$files_one], + ['3_folders_mac.zip', true, 9, self::$directories_three, 12, self::$files_three], + ['3_folders_win.zip', true, 9, self::$directories_three, 12, self::$files_three], + ['1_folder_1_file_mac.zip', true, 3, self::$directories_mixed, 5, self::$files_mixed] ]; } - protected array $files_mixed = [ + protected static array $files_mixed = [ 0 => '03_Test.pdf', 1 => 'Ordner A/01_Test.pdf', 2 => 'Ordner A/02_Test.pdf', @@ -231,13 +231,13 @@ public function getZips(): array 4 => 'Ordner A/Ordner A_2/08_Test.pdf' ]; - protected array $directories_mixed = [ + protected static array $directories_mixed = [ 0 => 'Ordner A/', 1 => 'Ordner A/Ordner A_1/', 2 => 'Ordner A/Ordner A_2/' ]; - protected array $directories_one = [ + protected static array $directories_one = [ 0 => 'Ordner 0/', 1 => 'Ordner 0/Ordner A/', 2 => 'Ordner 0/Ordner A/Ordner A_1/', @@ -249,7 +249,7 @@ public function getZips(): array 8 => 'Ordner 0/Ordner C/Ordner C_1/', 9 => 'Ordner 0/Ordner C/Ordner C_2/' ]; - protected array $directories_three = [ + protected static array $directories_three = [ 0 => 'Ordner A/', 1 => 'Ordner A/Ordner A_1/', 2 => 'Ordner A/Ordner A_2/', @@ -261,7 +261,7 @@ public function getZips(): array 8 => 'Ordner C/Ordner C_2/' ]; - protected array $files_one = [ + protected static array $files_one = [ 0 => 'Ordner 0/13_Test.pdf', 1 => 'Ordner 0/14_Test.pdf', 2 => 'Ordner 0/15_Test.pdf', @@ -279,7 +279,7 @@ public function getZips(): array 14 => 'Ordner 0/Ordner C/Ordner C_2/12_Test.pdf' ]; - protected array $files_three = [ + protected static array $files_three = [ 0 => 'Ordner A/01_Test.pdf', 1 => 'Ordner A/02_Test.pdf', 2 => 'Ordner A/Ordner A_2/07_Test.pdf', @@ -294,7 +294,7 @@ public function getZips(): array 11 => 'Ordner C/Ordner C_2/12_Test.pdf', ]; - protected array $top_directory_tree = [ + protected static array $top_directory_tree = [ 0 => '3_folders_mac/', 1 => '3_folders_mac/Ordner A/', 2 => '3_folders_mac/Ordner A/01_Test.pdf', @@ -319,7 +319,7 @@ public function getZips(): array 21 => '3_folders_mac/Ordner C/Ordner C_2/12_Test.pdf', ]; - private array $expected_flat_files = [ + private static array $expected_flat_files = [ 0 => '01_Test.pdf', 1 => '02_Test.pdf', 2 => '03_Test.pdf', diff --git a/tests/Filesystem/Util/ZipTest.php b/tests/Filesystem/Util/ZipTest.php index 127a6644d075..9a592d1b6b60 100644 --- a/tests/Filesystem/Util/ZipTest.php +++ b/tests/Filesystem/Util/ZipTest.php @@ -45,6 +45,9 @@ protected function setUp(): void if (file_exists($this->unzips_dir . self::ZIPPED_ZIP)) { unlink($this->unzips_dir . self::ZIPPED_ZIP); } + if (!file_exists($this->unzips_dir)) { + mkdir($this->unzips_dir); + } } protected function tearDown(): void @@ -74,15 +77,11 @@ public function testLegacyZip(): void $legacy = new LegacyArchives(); define('CLIENT_WEB_DIR', __DIR__); - define('ILIAS_WEB_DIR', __DIR__); + define('ILIAS_WEB_DIR', 'public/data'); define('CLIENT_ID', 'test'); define('CLIENT_DATA_DIR', __DIR__); define('ILIAS_ABSOLUTE_PATH', __DIR__); - if (is_dir($this->unzips_dir)) { - $this->recurseRmdir($this->unzips_dir); - } - mkdir($this->unzips_dir); $legacy->zip($this->zips_dir, $this->unzips_dir . self::ZIPPED_ZIP, false); $this->assertFileExists($this->unzips_dir . self::ZIPPED_ZIP); @@ -94,7 +93,7 @@ public function testLegacyZip(): void $parts = explode('/', $path); $depth = max($depth, count($parts)); } - $this->assertEquals(1, $depth); + $this->assertEquals(2, $depth); $this->recurseRmdir($this->unzips_dir); } @@ -103,7 +102,7 @@ public function LegacyZipWithTop(): void $legacy = new LegacyArchives(); define('CLIENT_WEB_DIR', __DIR__); - define('ILIAS_WEB_DIR', __DIR__); + define('ILIAS_WEB_DIR', 'public/data'); define('CLIENT_ID', 'test'); define('CLIENT_DATA_DIR', __DIR__); define('ILIAS_ABSOLUTE_PATH', __DIR__); @@ -265,18 +264,18 @@ private function directoryToArray(string $path_to_directory): array // PROVIDERS - public function getZips(): array + public static function getZips(): array { return [ - ['1_folder_mac.zip', false, 10, $this->directories_one, 15, $this->files_one], - ['1_folder_win.zip', false, 10, $this->directories_one, 15, $this->files_one], - ['3_folders_mac.zip', true, 9, $this->directories_three, 12, $this->files_three], - ['3_folders_win.zip', true, 9, $this->directories_three, 12, $this->files_three], - ['1_folder_1_file_mac.zip', true, 3, $this->directories_mixed, 5, $this->files_mixed] + ['1_folder_mac.zip', false, 10, self::$directories_one, 15, self::$files_one], + ['1_folder_win.zip', false, 10, self::$directories_one, 15, self::$files_one], + ['3_folders_mac.zip', true, 9, self::$directories_three, 12, self::$files_three], + ['3_folders_win.zip', true, 9, self::$directories_three, 12, self::$files_three], + ['1_folder_1_file_mac.zip', true, 3, self::$directories_mixed, 5, self::$files_mixed] ]; } - protected array $files_mixed = [ + protected static array $files_mixed = [ 0 => '03_Test.pdf', 1 => 'Ordner A/01_Test.pdf', 2 => 'Ordner A/02_Test.pdf', @@ -284,13 +283,13 @@ public function getZips(): array 4 => 'Ordner A/Ordner A_2/08_Test.pdf' ]; - protected array $directories_mixed = [ + protected static array $directories_mixed = [ 0 => 'Ordner A/', 1 => 'Ordner A/Ordner A_1/', 2 => 'Ordner A/Ordner A_2/' ]; - protected array $directories_one = [ + protected static array $directories_one = [ 0 => 'Ordner 0/', 1 => 'Ordner 0/Ordner A/', 2 => 'Ordner 0/Ordner A/Ordner A_1/', @@ -302,7 +301,7 @@ public function getZips(): array 8 => 'Ordner 0/Ordner C/Ordner C_1/', 9 => 'Ordner 0/Ordner C/Ordner C_2/' ]; - protected array $directories_three = [ + protected static array $directories_three = [ 0 => 'Ordner A/', 1 => 'Ordner A/Ordner A_1/', 2 => 'Ordner A/Ordner A_2/', @@ -314,7 +313,7 @@ public function getZips(): array 8 => 'Ordner C/Ordner C_2/' ]; - protected array $files_one = [ + protected static array $files_one = [ 0 => 'Ordner 0/13_Test.pdf', 1 => 'Ordner 0/14_Test.pdf', 2 => 'Ordner 0/15_Test.pdf', @@ -332,7 +331,7 @@ public function getZips(): array 14 => 'Ordner 0/Ordner C/Ordner C_2/12_Test.pdf' ]; - protected array $files_three = [ + protected static array $files_three = [ 0 => 'Ordner A/01_Test.pdf', 1 => 'Ordner A/02_Test.pdf', 2 => 'Ordner A/Ordner A_2/07_Test.pdf',