diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 85af06d4ca1..c096d8a757a 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -61,8 +61,10 @@ use OCP\Collaboration\Reference\RenderReferenceEvent; use OCP\Collaboration\Resources\IProviderManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Cache\CacheEntryRemovedEvent; +use OCP\Files\Events\Node\BeforeNodeCopiedEvent; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\Events\Node\BeforeNodeRenamedEvent; +use OCP\Files\Events\Node\NodeCopiedEvent; use OCP\IConfig; use OCP\IPreview; use OCP\IRequest; @@ -132,6 +134,8 @@ class Application extends App implements IBootstrap { $context->registerEventListener(BeforeNodeRestoredEvent::class, SyncLivePhotosListener::class); $context->registerEventListener(CacheEntryRemovedEvent::class, SyncLivePhotosListener::class); $context->registerEventListener(LoadSearchPlugins::class, LoadSearchPluginsListener::class); + $context->registerEventListener(BeforeNodeCopiedEvent::class, SyncLivePhotosListener::class); + $context->registerEventListener(NodeCopiedEvent::class, SyncLivePhotosListener::class); $context->registerSearchProvider(FilesSearchProvider::class); diff --git a/apps/files/lib/Listener/SyncLivePhotosListener.php b/apps/files/lib/Listener/SyncLivePhotosListener.php index ea63c08f94d..8f8234428d6 100644 --- a/apps/files/lib/Listener/SyncLivePhotosListener.php +++ b/apps/files/lib/Listener/SyncLivePhotosListener.php @@ -29,9 +29,13 @@ use OCA\Files_Trashbin\Trash\ITrashItem; use OCA\Files_Trashbin\Trash\ITrashManager; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; +use OCP\Exceptions\AbortedEventException; use OCP\Files\Cache\CacheEntryRemovedEvent; +use OCP\Files\Events\Node\AbstractNodesEvent; +use OCP\Files\Events\Node\BeforeNodeCopiedEvent; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\Events\Node\BeforeNodeRenamedEvent; +use OCP\Files\Events\Node\NodeCopiedEvent; use OCP\Files\Folder; use OCP\Files\Node; use OCP\Files\NotFoundException; @@ -44,7 +48,7 @@ use OCP\IUserSession; * @template-implements IEventListener */ class SyncLivePhotosListener implements IEventListener { - /** @var Array */ + /** @var Array */ private array $pendingRenames = []; /** @var Array */ private array $pendingDeletion = []; @@ -65,7 +69,6 @@ class SyncLivePhotosListener implements IEventListener { } $peerFile = null; - if ($event instanceof BeforeNodeRenamedEvent) { $peerFile = $this->getLivePhotoPeer($event->getSource()->getId()); } elseif ($event instanceof BeforeNodeRestoredEvent) { @@ -74,6 +77,8 @@ class SyncLivePhotosListener implements IEventListener { $peerFile = $this->getLivePhotoPeer($event->getNode()->getId()); } elseif ($event instanceof CacheEntryRemovedEvent) { $peerFile = $this->getLivePhotoPeer($event->getFileId()); + } elseif ($event instanceof BeforeNodeCopiedEvent || $event instanceof NodeCopiedEvent) { + $peerFile = $this->getLivePhotoPeer($event->getSource()->getId()); } if ($peerFile === null) { @@ -81,13 +86,17 @@ class SyncLivePhotosListener implements IEventListener { } if ($event instanceof BeforeNodeRenamedEvent) { - $this->handleMove($event, $peerFile); + $this->handleMove($event, $peerFile, false); } elseif ($event instanceof BeforeNodeDeletedEvent) { $this->handleDeletion($event, $peerFile); } elseif ($event instanceof CacheEntryRemovedEvent) { $peerFile->delete(); } elseif ($event instanceof BeforeNodeRestoredEvent) { $this->handleRestore($event, $peerFile); + } elseif ($event instanceof BeforeNodeCopiedEvent) { + $this->handleMove($event, $peerFile, true); + } elseif ($event instanceof NodeCopiedEvent) { + $this->handleCopy($event, $peerFile); } } @@ -98,44 +107,79 @@ class SyncLivePhotosListener implements IEventListener { * of pending renames inside the 'pendingRenames' property, * to prevent infinite recursive. */ - private function handleMove(BeforeNodeRenamedEvent $event, Node $peerFile): void { + private function handleMove(AbstractNodesEvent $event, Node $peerFile, bool $prepForCopyOnly = false): void { + if (!($event instanceof BeforeNodeCopiedEvent) && + !($event instanceof BeforeNodeRenamedEvent)) { + return; + } + $sourceFile = $event->getSource(); $targetFile = $event->getTarget(); $targetParent = $targetFile->getParent(); $sourceExtension = $sourceFile->getExtension(); $peerFileExtension = $peerFile->getExtension(); $targetName = $targetFile->getName(); - $targetPath = $targetFile->getPath(); - if (!str_ends_with($targetName, ".".$sourceExtension)) { - $event->abortOperation(new NotPermittedException("Cannot change the extension of a Live Photo")); + if (!str_ends_with($targetName, "." . $sourceExtension)) { + throw new AbortedEventException('Cannot change the extension of a Live Photo'); } try { $targetParent->get($targetName); - $event->abortOperation(new NotPermittedException("A file already exist at destination path of the Live Photo")); - } catch (NotFoundException $ex) { + throw new AbortedEventException('A file already exist at destination path of the Live Photo'); + } catch (NotFoundException) { } $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension; try { $targetParent->get($peerTargetName); - $event->abortOperation(new NotPermittedException("A file already exist at destination path of the Live Photo")); - } catch (NotFoundException $ex) { + throw new AbortedEventException('A file already exist at destination path of the Live Photo'); + } catch (NotFoundException) { } // in case the rename was initiated from this listener, we stop right now - if (array_key_exists($peerFile->getId(), $this->pendingRenames)) { + if ($prepForCopyOnly || in_array($peerFile->getId(), $this->pendingRenames)) { return; } - $this->pendingRenames[$sourceFile->getId()] = $targetPath; + $this->pendingRenames[] = $sourceFile->getId(); try { $peerFile->move($targetParent->getPath() . '/' . $peerTargetName); } catch (\Throwable $ex) { - $event->abortOperation($ex); + throw new AbortedEventException($ex->getMessage()); } - unset($this->pendingRenames[$sourceFile->getId()]); + + array_diff($this->pendingRenames, [$sourceFile->getId()]); + } + + + /** + * handle copy, we already know if it is doable from BeforeNodeCopiedEvent, so we just copy the linked file + * + * @param NodeCopiedEvent $event + * @param Node $peerFile + */ + private function handleCopy(NodeCopiedEvent $event, Node $peerFile): void { + $sourceFile = $event->getSource(); + $sourceExtension = $sourceFile->getExtension(); + $peerFileExtension = $peerFile->getExtension(); + $targetFile = $event->getTarget(); + $targetParent = $targetFile->getParent(); + $targetName = $targetFile->getName(); + $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension; + + /** + * let's use freshly set variable. + * we copy the file and get its id. We already have the id of the current copy + * We have everything to update metadata and keep the link between the 2 copies. + */ + $newPeerFile = $peerFile->copy($targetParent->getPath() . '/' . $peerTargetName); + $targetMetadata = $this->filesMetadataManager->getMetadata($targetFile->getId(), true); + $targetMetadata->setString('files-live-photo', (string)$newPeerFile->getId()); + $this->filesMetadataManager->saveMetadata($targetMetadata); + $peerMetadata = $this->filesMetadataManager->getMetadata($newPeerFile->getId(), true); + $peerMetadata->setString('files-live-photo', (string)$targetFile->getId()); + $this->filesMetadataManager->saveMetadata($peerMetadata); } /** @@ -152,14 +196,14 @@ class SyncLivePhotosListener implements IEventListener { unset($this->pendingDeletion[$peerFile->getId()]); return; } else { - $event->abortOperation(new NotPermittedException("Cannot delete the video part of a live photo")); + throw new AbortedEventException("Cannot delete the video part of a live photo"); } } else { $this->pendingDeletion[$deletedFile->getId()] = true; try { $peerFile->delete(); } catch (\Throwable $ex) { - $event->abortOperation($ex); + throw new AbortedEventException($ex->getMessage()); } } return; diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index e32e509e3ee..936d8c7cce2 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -276,6 +276,7 @@ return array( 'OCP\\EventDispatcher\\GenericEvent' => $baseDir . '/lib/public/EventDispatcher/GenericEvent.php', 'OCP\\EventDispatcher\\IEventDispatcher' => $baseDir . '/lib/public/EventDispatcher/IEventDispatcher.php', 'OCP\\EventDispatcher\\IEventListener' => $baseDir . '/lib/public/EventDispatcher/IEventListener.php', + 'OCP\\Exceptions\\AbortedEventException' => $baseDir . '/lib/public/Exceptions/AbortedEventException.php', 'OCP\\Exceptions\\AppConfigException' => $baseDir . '/lib/public/Exceptions/AppConfigException.php', 'OCP\\Exceptions\\AppConfigIncorrectTypeException' => $baseDir . '/lib/public/Exceptions/AppConfigIncorrectTypeException.php', 'OCP\\Exceptions\\AppConfigTypeConflictException' => $baseDir . '/lib/public/Exceptions/AppConfigTypeConflictException.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 2e8a3c71c0a..9294cce9a0d 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -309,6 +309,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\EventDispatcher\\GenericEvent' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/GenericEvent.php', 'OCP\\EventDispatcher\\IEventDispatcher' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/IEventDispatcher.php', 'OCP\\EventDispatcher\\IEventListener' => __DIR__ . '/../../..' . '/lib/public/EventDispatcher/IEventListener.php', + 'OCP\\Exceptions\\AbortedEventException' => __DIR__ . '/../../..' . '/lib/public/Exceptions/AbortedEventException.php', 'OCP\\Exceptions\\AppConfigException' => __DIR__ . '/../../..' . '/lib/public/Exceptions/AppConfigException.php', 'OCP\\Exceptions\\AppConfigIncorrectTypeException' => __DIR__ . '/../../..' . '/lib/public/Exceptions/AppConfigIncorrectTypeException.php', 'OCP\\Exceptions\\AppConfigTypeConflictException' => __DIR__ . '/../../..' . '/lib/public/Exceptions/AppConfigTypeConflictException.php', diff --git a/lib/private/Files/Node/HookConnector.php b/lib/private/Files/Node/HookConnector.php index f61eedee66e..d66c7a60664 100644 --- a/lib/private/Files/Node/HookConnector.php +++ b/lib/private/Files/Node/HookConnector.php @@ -6,6 +6,7 @@ declare(strict_types=1); * @copyright Copyright (c) 2016, ownCloud, Inc. * * @author Arthur Schiwon + * @author Maxence Lange * @author Robin Appelman * @author Roeland Jago Douma * @@ -30,6 +31,7 @@ use OC\Files\Filesystem; use OC\Files\View; use OCP\EventDispatcher\GenericEvent; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Exceptions\AbortedEventException; use OCP\Files\Events\Node\BeforeNodeCopiedEvent; use OCP\Files\Events\Node\BeforeNodeCreatedEvent; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; @@ -46,27 +48,18 @@ use OCP\Files\Events\Node\NodeWrittenEvent; use OCP\Files\FileInfo; use OCP\Files\IRootFolder; use OCP\Util; +use Psr\Log\LoggerInterface; class HookConnector { - /** @var IRootFolder */ - private $root; - - /** @var View */ - private $view; - /** @var FileInfo[] */ - private $deleteMetaCache = []; - - /** @var IEventDispatcher */ - private $dispatcher; + private array $deleteMetaCache = []; public function __construct( - IRootFolder $root, - View $view, - IEventDispatcher $dispatcher) { - $this->root = $root; - $this->view = $view; - $this->dispatcher = $dispatcher; + private IRootFolder $root, + private View $view, + private IEventDispatcher $dispatcher, + private LoggerInterface $logger + ) { } public function viewToNode() { @@ -133,8 +126,13 @@ class HookConnector { $this->root->emit('\OC\Files', 'preDelete', [$node]); $this->dispatcher->dispatch('\OCP\Files::preDelete', new GenericEvent($node)); - $event = new BeforeNodeDeletedEvent($node, $arguments['run']); - $this->dispatcher->dispatchTyped($event); + $event = new BeforeNodeDeletedEvent($node); + try { + $this->dispatcher->dispatchTyped($event); + } catch (AbortedEventException $e) { + $arguments['run'] = false; + $this->logger->warning('delete process aborted', ['exception' => $e]); + } } public function postDelete($arguments) { @@ -171,8 +169,13 @@ class HookConnector { $this->root->emit('\OC\Files', 'preRename', [$source, $target]); $this->dispatcher->dispatch('\OCP\Files::preRename', new GenericEvent([$source, $target])); - $event = new BeforeNodeRenamedEvent($source, $target, $arguments['run']); - $this->dispatcher->dispatchTyped($event); + $event = new BeforeNodeRenamedEvent($source, $target); + try { + $this->dispatcher->dispatchTyped($event); + } catch (AbortedEventException $e) { + $arguments['run'] = false; + $this->logger->warning('rename process aborted', ['exception' => $e]); + } } public function postRename($arguments) { @@ -192,7 +195,12 @@ class HookConnector { $this->dispatcher->dispatch('\OCP\Files::preCopy', new GenericEvent([$source, $target])); $event = new BeforeNodeCopiedEvent($source, $target); - $this->dispatcher->dispatchTyped($event); + try { + $this->dispatcher->dispatchTyped($event); + } catch (AbortedEventException $e) { + $arguments['run'] = false; + $this->logger->warning('copy process aborted', ['exception' => $e]); + } } public function postCopy($arguments) { diff --git a/lib/private/Server.php b/lib/private/Server.php index 2732b6486f2..a29280a09ea 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -475,7 +475,8 @@ class Server extends ServerContainer implements IServerContainer { return new HookConnector( $c->get(IRootFolder::class), new View(), - $c->get(IEventDispatcher::class) + $c->get(IEventDispatcher::class), + $c->get(LoggerInterface::class) ); }); diff --git a/lib/public/Exceptions/AbortedEventException.php b/lib/public/Exceptions/AbortedEventException.php new file mode 100644 index 00000000000..5a3b797e386 --- /dev/null +++ b/lib/public/Exceptions/AbortedEventException.php @@ -0,0 +1,34 @@ + + * + * @author Maxence Lange + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Exceptions; + +use Exception; + +/** + * @since 29.0.0 + */ +class AbortedEventException extends Exception { +} diff --git a/lib/public/Files/Events/Node/AbstractNodeEvent.php b/lib/public/Files/Events/Node/AbstractNodeEvent.php index d6c1c6d0f95..768c0eda2d5 100644 --- a/lib/public/Files/Events/Node/AbstractNodeEvent.php +++ b/lib/public/Files/Events/Node/AbstractNodeEvent.php @@ -32,14 +32,12 @@ use OCP\Files\Node; * @since 20.0.0 */ abstract class AbstractNodeEvent extends Event { - /** @var Node */ - private $node; - /** * @since 20.0.0 */ - public function __construct(Node $node) { - $this->node = $node; + public function __construct( + private Node $node + ) { } /** diff --git a/lib/public/Files/Events/Node/AbstractNodesEvent.php b/lib/public/Files/Events/Node/AbstractNodesEvent.php index 6bd9bc32a88..d736ebf1635 100644 --- a/lib/public/Files/Events/Node/AbstractNodesEvent.php +++ b/lib/public/Files/Events/Node/AbstractNodesEvent.php @@ -32,17 +32,13 @@ use OCP\Files\Node; * @since 20.0.0 */ abstract class AbstractNodesEvent extends Event { - /** @var Node */ - private $source; - /** @var Node */ - private $target; - /** * @since 20.0.0 */ - public function __construct(Node $source, Node $target) { - $this->source = $source; - $this->target = $target; + public function __construct( + private Node $source, + private Node $target + ) { } /** diff --git a/lib/public/Files/Events/Node/BeforeNodeDeletedEvent.php b/lib/public/Files/Events/Node/BeforeNodeDeletedEvent.php index dd29a39a279..c0226a9b527 100644 --- a/lib/public/Files/Events/Node/BeforeNodeDeletedEvent.php +++ b/lib/public/Files/Events/Node/BeforeNodeDeletedEvent.php @@ -25,31 +25,17 @@ declare(strict_types=1); */ namespace OCP\Files\Events\Node; -use Exception; -use OCP\Files\Node; +use OCP\Exceptions\AbortedEventException; /** * @since 20.0.0 */ class BeforeNodeDeletedEvent extends AbstractNodeEvent { - /** - * @since 20.0.0 - */ - public function __construct(Node $node, private bool &$run) { - parent::__construct($node); - } - /** * @since 28.0.0 - * @return never + * @deprecated 29.0.0 - use OCP\Exceptions\AbortedEventException instead */ public function abortOperation(\Throwable $ex = null) { - $this->stopPropagation(); - $this->run = false; - if ($ex !== null) { - throw $ex; - } else { - throw new Exception('Operation aborted'); - } + throw new AbortedEventException($ex?->getMessage() ?? 'Operation aborted'); } } diff --git a/lib/public/Files/Events/Node/BeforeNodeRenamedEvent.php b/lib/public/Files/Events/Node/BeforeNodeRenamedEvent.php index c6876666713..4c2c566c8c6 100644 --- a/lib/public/Files/Events/Node/BeforeNodeRenamedEvent.php +++ b/lib/public/Files/Events/Node/BeforeNodeRenamedEvent.php @@ -25,31 +25,17 @@ declare(strict_types=1); */ namespace OCP\Files\Events\Node; -use Exception; -use OCP\Files\Node; +use OCP\Exceptions\AbortedEventException; /** * @since 20.0.0 */ class BeforeNodeRenamedEvent extends AbstractNodesEvent { - /** - * @since 20.0.0 - */ - public function __construct(Node $source, Node $target, private bool &$run) { - parent::__construct($source, $target); - } - /** * @since 28.0.0 - * @return never + * @deprecated 29.0.0 - use OCP\Exceptions\AbortedEventException instead */ public function abortOperation(\Throwable $ex = null) { - $this->stopPropagation(); - $this->run = false; - if ($ex !== null) { - throw $ex; - } else { - throw new Exception('Operation aborted'); - } + throw new AbortedEventException($ex?->getMessage() ?? 'Operation aborted'); } } diff --git a/tests/lib/Files/Node/HookConnectorTest.php b/tests/lib/Files/Node/HookConnectorTest.php index 80a94462bba..c64052ad172 100644 --- a/tests/lib/Files/Node/HookConnectorTest.php +++ b/tests/lib/Files/Node/HookConnectorTest.php @@ -53,6 +53,8 @@ class HookConnectorTest extends TestCase { /** @var IEventDispatcher */ protected $eventDispatcher; + private LoggerInterface $logger; + /** @var View */ private $view; @@ -86,6 +88,7 @@ class HookConnectorTest extends TestCase { $cacheFactory, ); $this->eventDispatcher = \OC::$server->query(IEventDispatcher::class); + $this->logger = \OC::$server->query(LoggerInterface::class); } protected function tearDown(): void { @@ -151,7 +154,7 @@ class HookConnectorTest extends TestCase { * @dataProvider viewToNodeProvider */ public function testViewToNode(callable $operation, $expectedHook, $expectedLegacyEvent, $expectedEvent) { - $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher); + $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher, $this->logger); $connector->viewToNode(); $hookCalled = false; /** @var Node $hookNode */ @@ -220,7 +223,7 @@ class HookConnectorTest extends TestCase { * @dataProvider viewToNodeProviderCopyRename */ public function testViewToNodeCopyRename(callable $operation, $expectedHook, $expectedLegacyEvent, $expectedEvent) { - $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher); + $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher, $this->logger); $connector->viewToNode(); $hookCalled = false; /** @var Node $hookSourceNode */ @@ -275,7 +278,7 @@ class HookConnectorTest extends TestCase { } public function testPostDeleteMeta() { - $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher); + $connector = new HookConnector($this->root, $this->view, $this->eventDispatcher, $this->logger); $connector->viewToNode(); $hookCalled = false; /** @var Node $hookNode */