diff --git a/src/Filesystem/Doctrine/EventListener/NodeLifecycleListener.php b/src/Filesystem/Doctrine/EventListener/NodeLifecycleListener.php index ead78fb0..035f1ce5 100644 --- a/src/Filesystem/Doctrine/EventListener/NodeLifecycleListener.php +++ b/src/Filesystem/Doctrine/EventListener/NodeLifecycleListener.php @@ -29,11 +29,11 @@ use Zenstruck\Filesystem\Node\File; use Zenstruck\Filesystem\Node\File\Image; use Zenstruck\Filesystem\Node\File\Image\LazyImage; -use Zenstruck\Filesystem\Node\File\Image\PendingImage; use Zenstruck\Filesystem\Node\File\Image\SerializableImage; use Zenstruck\Filesystem\Node\File\LazyFile; use Zenstruck\Filesystem\Node\File\PendingFile; use Zenstruck\Filesystem\Node\File\SerializableFile; +use Zenstruck\Filesystem\Node\File\TemporaryFile; use Zenstruck\Filesystem\Node\Mapping; use Zenstruck\Filesystem\Node\PathGenerator; @@ -143,7 +143,10 @@ public function prePersist(LifecycleEventArgs $event): void $original = $metadata->getFieldValue($object, $field); $new = null; - if ($original instanceof PendingFile) { + if ( + $original instanceof PendingFile + || $original instanceof TemporaryFile + ) { $new = $this->convertPendingFile($mapping, $original, $object, $field); } @@ -176,7 +179,10 @@ public function preUpdate(PreUpdateEventArgs|ORMPreUpdateEventArgs $event): void $old = $event->getOldValue($field); $new = $event->getNewValue($field); - if ($new instanceof PendingFile) { + if ( + $new instanceof PendingFile + || $new instanceof TemporaryFile + ) { $new = $this->convertPendingFile($mapping, $new, $object, $field); // just setting the new value does not update the property so refresh the object on flush @@ -231,7 +237,7 @@ private static function createSerialized(StoreWithMetadata $mapping, File $file) return new SerializableFile($file, $mapping->metadata); } - private function convertPendingFile(Mapping $mapping, PendingFile $file, object $object, string $field): LazyFile + private function convertPendingFile(Mapping $mapping, PendingFile|TemporaryFile $file, object $object, string $field): LazyFile { if (!$mapping->filesystem()) { throw new \LogicException(\sprintf('In order to save pending files, the %s::$%s mapping must have a filesystem configured.', $object::class, $field)); @@ -246,11 +252,16 @@ private function convertPendingFile(Mapping $mapping, PendingFile $file, object $this->postFlushOperations[] = fn() => $this->filesystem($mapping)->write($path, $file); } + if ($file instanceof TemporaryFile) { + $filesystem = $this->filesystem($mapping); + $this->postFlushOperations[] = static fn() => $filesystem->delete($filesystem->directory($path)->path()); + } + if ($mapping instanceof StoreAsDsn) { $path = Dsn::create($mapping->filesystem(), $path); } - $lazyFile = $file instanceof PendingImage ? new LazyImage($path) : new LazyFile($path); + $lazyFile = $file instanceof Image ? new LazyImage($path) : new LazyFile($path); $lazyFile->setFilesystem($this->filesystem($mapping)); return $lazyFile; diff --git a/src/Filesystem/Glide/GlideTransformUrlGenerator.php b/src/Filesystem/Glide/GlideTransformUrlGenerator.php index 0ea30e3e..b9dfc034 100644 --- a/src/Filesystem/Glide/GlideTransformUrlGenerator.php +++ b/src/Filesystem/Glide/GlideTransformUrlGenerator.php @@ -28,7 +28,7 @@ public function transformUrl(string $path, array|string $filter, Config $config) { $filter = match (true) { // @phpstan-ignore-line https://github.com/phpstan/phpstan/issues/8937 \is_string($filter) => ['p' => $filter], // is glide "preset" - \is_array($filter) && !array_is_list($filter) => $filter, // is standard glide parameters + \is_array($filter) && !\array_is_list($filter) => $filter, // is standard glide parameters \is_array($filter) => ['p' => \implode(',', $filter)], // is array of "presets" }; diff --git a/src/Filesystem/Node/File/Image/TemporaryImage.php b/src/Filesystem/Node/File/Image/TemporaryImage.php new file mode 100644 index 00000000..6e692f05 --- /dev/null +++ b/src/Filesystem/Node/File/Image/TemporaryImage.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Filesystem\Node\File\Image; + +use Zenstruck\Filesystem\Node\File\Image; +use Zenstruck\Filesystem\Node\File\TemporaryFile; + +/** + * @author Jakub Caban + */ +final class TemporaryImage extends TemporaryFile implements Image +{ + use DecoratedImage; + + public function __construct(private Image $image) + { + } + + protected function inner(): Image + { + return $this->image; + } +} diff --git a/src/Filesystem/Node/File/PendingFile.php b/src/Filesystem/Node/File/PendingFile.php index b2e9e435..fe239cbd 100644 --- a/src/Filesystem/Node/File/PendingFile.php +++ b/src/Filesystem/Node/File/PendingFile.php @@ -47,7 +47,7 @@ public function __construct(string|\SplFileInfo $filename) } /** - * @param callable(self):string $path + * @param string|null|callable(self):string $path */ public function saveTo(Filesystem $filesystem, string|callable|null $path = null): static { @@ -60,6 +60,15 @@ public function saveTo(Filesystem $filesystem, string|callable|null $path = null return $this; } + public function saveToTemporary(Filesystem $filesystem): File + { + do { + $directory = (string) \microtime(); + } while ($filesystem->has($directory)); + + return $filesystem->write($directory.'/.'.$this->path()->name(), $this); + } + public function path(): Path { if (isset($this->path)) { diff --git a/src/Filesystem/Node/File/TemporaryFile.php b/src/Filesystem/Node/File/TemporaryFile.php new file mode 100644 index 00000000..c0cf7355 --- /dev/null +++ b/src/Filesystem/Node/File/TemporaryFile.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Filesystem\Node\File; + +use League\Flysystem\FilesystemException; +use Zenstruck\Filesystem\Node; +use Zenstruck\Filesystem\Node\DecoratedNode; +use Zenstruck\Filesystem\Node\File; +use Zenstruck\Filesystem\Node\File\Image\TemporaryImage; + +/** + * @author Jakub Caban + */ +class TemporaryFile implements File +{ + use DecoratedFile, DecoratedNode; + + public function __construct(private File $file) + { + } + + public function ensureFile(): File + { + return $this->ensureTemporary( + $this->inner()->ensureFile() + ); + } + + public function ensureImage(): Image + { + $image = $this->ensureTemporary( + $this->inner()->ensureImage() + ); + \assert($image instanceof TemporaryImage); + + return $image; + } + + protected function inner(): File + { + return $this->file; + } + + /** + * @throws FilesystemException + */ + private function ensureTemporary(Node $node): self + { + if ($node instanceof self) { + return $node; + } + + if ($node instanceof Image) { + return new TemporaryImage($node); + } + + return new self($node->ensureFile()); + } +} diff --git a/src/Filesystem/Symfony/DependencyInjection/Configuration.php b/src/Filesystem/Symfony/DependencyInjection/Configuration.php index d54c5314..d6e12fe2 100644 --- a/src/Filesystem/Symfony/DependencyInjection/Configuration.php +++ b/src/Filesystem/Symfony/DependencyInjection/Configuration.php @@ -266,6 +266,13 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->booleanNode('temporary') + ->defaultFalse() + ->info(<<end() ->booleanNode('reset_before_tests') ->defaultFalse() ->info(<<register('.zenstruck_filesystem.filesystem.temporary_'.$name, TemporaryFilesystem::class) + ->setDecoratedService($filesystemId) + ->setArguments([new Reference('.inner')]) + ; + } + if ($config['log']['enabled']) { $container->register('.zenstruck_filesystem.filesystem.log_'.$name, LoggableFilesystem::class) ->setDecoratedService($filesystemId) diff --git a/src/Filesystem/TemporaryFilesystem.php b/src/Filesystem/TemporaryFilesystem.php new file mode 100644 index 00000000..f31b5c6b --- /dev/null +++ b/src/Filesystem/TemporaryFilesystem.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Filesystem; + +use League\Flysystem\FilesystemException; +use Zenstruck\Filesystem; +use Zenstruck\Filesystem\Node\File; +use Zenstruck\Filesystem\Node\File\Image; +use Zenstruck\Filesystem\Node\File\Image\TemporaryImage; +use Zenstruck\Filesystem\Node\File\TemporaryFile; + +/** + * @author Jakub Caban + */ +class TemporaryFilesystem implements Filesystem +{ + use DecoratedFilesystem; + + public function __construct(private Filesystem $inner) + { + } + + public function node(string $path): Node + { + $node = $this->inner()->node($path); + + if ($node instanceof File) { + return $this->ensureTemporary($node); + } + + return $node; + } + + public function file(string $path): File + { + return $this->ensureTemporary( + $this->inner()->file($path) + ); + } + + public function image(string $path): Image + { + $image = $this->ensureTemporary( + $this->inner()->image($path) + ); + \assert($image instanceof TemporaryImage); + + return $image; + } + + public function copy(string $source, string $destination, array $config = []): File + { + return $this->ensureTemporary( + $this->inner()->copy($source, $destination, $config) + ); + } + + public function move(string $source, string $destination, array $config = []): File + { + return $this->ensureTemporary( + $this->inner()->move($source, $destination, $config) + ); + } + + public function chmod(string $path, string $visibility): Node + { + return $this->ensureTemporary( + $this->inner()->chmod($path, $visibility) + ); + } + + public function write(string $path, mixed $value, array $config = []): File + { + return $this->ensureTemporary( + $this->inner()->write($path, $value, $config) + ); + } + + protected function inner(): Filesystem + { + return $this->inner; + } + + /** + * @throws FilesystemException + */ + private function ensureTemporary(Node $node): TemporaryFile + { + if ($node instanceof TemporaryFile) { + return $node; + } + + if ($node instanceof Image) { + return new TemporaryImage($node); + } + + return new TemporaryFile($node->ensureFile()); + } +} diff --git a/tests/Filesystem/TemporaryFilesystemTest.php b/tests/Filesystem/TemporaryFilesystemTest.php new file mode 100644 index 00000000..dc35dcfd --- /dev/null +++ b/tests/Filesystem/TemporaryFilesystemTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Tests\Filesystem; + +use Zenstruck\Filesystem; +use Zenstruck\Filesystem\TemporaryFilesystem; +use Zenstruck\Tests\FilesystemTest; + +/** + * @author Jakub Caban + */ +class TemporaryFilesystemTest extends FilesystemTest +{ + protected function createFilesystem(): Filesystem + { + return new TemporaryFilesystem(in_memory_filesystem()); + } +}