]> source.dussan.org Git - nextcloud-server.git/commitdiff
Synchronize operation on live photo files 41765/head
authorLouis Chemineau <louis@chmn.me>
Wed, 29 Nov 2023 18:05:35 +0000 (19:05 +0100)
committerLouis Chemineau <louis@chmn.me>
Wed, 29 Nov 2023 18:07:32 +0000 (19:07 +0100)
Signed-off-by: Louis Chemineau <louis@chmn.me>
13 files changed:
apps/files/composer/composer/autoload_classmap.php
apps/files/composer/composer/autoload_static.php
apps/files/composer/composer/installed.php
apps/files/lib/AppInfo/Application.php
apps/files/lib/Listener/SyncLivePhotosListener.php [new file with mode: 0644]
apps/files_trashbin/composer/composer/autoload_classmap.php
apps/files_trashbin/composer/composer/autoload_static.php
apps/files_trashbin/lib/Events/BeforeNodeRestoredEvent.php [new file with mode: 0644]
apps/files_trashbin/lib/Events/NodeRestoredEvent.php [new file with mode: 0644]
apps/files_trashbin/lib/Trashbin.php
lib/private/Files/Node/HookConnector.php
lib/public/Files/Events/Node/BeforeNodeDeletedEvent.php
lib/public/Files/Events/Node/BeforeNodeRenamedEvent.php

index 0f3e01b5b53f7fab2b61d67594f64be92b266205..d5874228e4662155b23e8676cf07148b593c6a28 100644 (file)
@@ -59,6 +59,7 @@ return array(
     'OCA\\Files\\Helper' => $baseDir . '/../lib/Helper.php',
     'OCA\\Files\\Listener\\LoadSidebarListener' => $baseDir . '/../lib/Listener/LoadSidebarListener.php',
     'OCA\\Files\\Listener\\RenderReferenceEventListener' => $baseDir . '/../lib/Listener/RenderReferenceEventListener.php',
+    'OCA\\Files\\Listener\\SyncLivePhotosListener' => $baseDir . '/../lib/Listener/SyncLivePhotosListener.php',
     'OCA\\Files\\Migration\\Version11301Date20191205150729' => $baseDir . '/../lib/Migration/Version11301Date20191205150729.php',
     'OCA\\Files\\Migration\\Version12101Date20221011153334' => $baseDir . '/../lib/Migration/Version12101Date20221011153334.php',
     'OCA\\Files\\Notification\\Notifier' => $baseDir . '/../lib/Notification/Notifier.php',
index 2b152c734f118a0d9344f6e9f741b4f90f9932ef..d5b1947c1cc9ee93f2b70f0ca8c5c4c19abe0fd1 100644 (file)
@@ -74,6 +74,7 @@ class ComposerStaticInitFiles
         'OCA\\Files\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php',
         'OCA\\Files\\Listener\\LoadSidebarListener' => __DIR__ . '/..' . '/../lib/Listener/LoadSidebarListener.php',
         'OCA\\Files\\Listener\\RenderReferenceEventListener' => __DIR__ . '/..' . '/../lib/Listener/RenderReferenceEventListener.php',
+        'OCA\\Files\\Listener\\SyncLivePhotosListener' => __DIR__ . '/..' . '/../lib/Listener/SyncLivePhotosListener.php',
         'OCA\\Files\\Migration\\Version11301Date20191205150729' => __DIR__ . '/..' . '/../lib/Migration/Version11301Date20191205150729.php',
         'OCA\\Files\\Migration\\Version12101Date20221011153334' => __DIR__ . '/..' . '/../lib/Migration/Version12101Date20221011153334.php',
         'OCA\\Files\\Notification\\Notifier' => __DIR__ . '/..' . '/../lib/Notification/Notifier.php',
index 1a66c7f2416b68419cd368ba532323cea42c8d90..b42781625c734c20b7ee6a9ac420f79b13081336 100644 (file)
@@ -3,7 +3,7 @@
         'name' => '__root__',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
+        'reference' => 'ba1af2b22e5409c62ea2bdf7eb0e13c282ed70e8',
         'type' => 'library',
         'install_path' => __DIR__ . '/../',
         'aliases' => array(),
@@ -13,7 +13,7 @@
         '__root__' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
+            'reference' => 'ba1af2b22e5409c62ea2bdf7eb0e13c282ed70e8',
             'type' => 'library',
             'install_path' => __DIR__ . '/../',
             'aliases' => array(),
index 5934bc1c7ce9853bd22c39a457bd3708e12c7ba7..41423a65ca7fe45ad0b2905897a0d3df089d01fd 100644 (file)
@@ -43,11 +43,13 @@ use OCA\Files\DirectEditingCapabilities;
 use OCA\Files\Event\LoadSidebar;
 use OCA\Files\Listener\LoadSidebarListener;
 use OCA\Files\Listener\RenderReferenceEventListener;
+use OCA\Files\Listener\SyncLivePhotosListener;
 use OCA\Files\Notification\Notifier;
 use OCA\Files\Search\FilesSearchProvider;
 use OCA\Files\Service\TagService;
 use OCA\Files\Service\UserConfig;
 use OCA\Files\Service\ViewConfig;
+use OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent;
 use OCP\Activity\IManager as IActivityManager;
 use OCP\AppFramework\App;
 use OCP\AppFramework\Bootstrap\IBootContext;
@@ -56,6 +58,9 @@ use OCP\AppFramework\Bootstrap\IRegistrationContext;
 use OCP\Collaboration\Reference\RenderReferenceEvent;
 use OCP\Collaboration\Resources\IProviderManager;
 use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Cache\CacheEntryRemovedEvent;
+use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
+use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
 use OCP\IConfig;
 use OCP\IPreview;
 use OCP\IRequest;
@@ -120,6 +125,10 @@ class Application extends App implements IBootstrap {
 
                $context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class);
                $context->registerEventListener(RenderReferenceEvent::class, RenderReferenceEventListener::class);
+               $context->registerEventListener(BeforeNodeRenamedEvent::class, SyncLivePhotosListener::class);
+               $context->registerEventListener(BeforeNodeDeletedEvent::class, SyncLivePhotosListener::class);
+               $context->registerEventListener(BeforeNodeRestoredEvent::class, SyncLivePhotosListener::class);
+               $context->registerEventListener(CacheEntryRemovedEvent::class, SyncLivePhotosListener::class);
 
                $context->registerSearchProvider(FilesSearchProvider::class);
 
diff --git a/apps/files/lib/Listener/SyncLivePhotosListener.php b/apps/files/lib/Listener/SyncLivePhotosListener.php
new file mode 100644 (file)
index 0000000..558b9e8
--- /dev/null
@@ -0,0 +1,252 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2023 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCA\Files\Listener;
+
+use OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent;
+use OCA\Files_Trashbin\Trash\ITrashItem;
+use OCA\Files_Trashbin\Trash\ITrashManager;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Files\Cache\CacheEntryRemovedEvent;
+use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
+use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
+use OCP\Files\Folder;
+use OCP\Files\Node;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException;
+use OCP\FilesMetadata\IFilesMetadataManager;
+use OCP\IUserSession;
+
+/**
+ * @template-implements IEventListener<Event>
+ */
+class SyncLivePhotosListener implements IEventListener {
+       /** @var Array<int, string> */
+       private $pendingRenames = [];
+       /** @var Array<int, bool> */
+       private $pendingDeletion = [];
+       /** @var Array<int, bool> */
+       private $pendingRestores = [];
+
+       public function __construct(
+               private ?Folder $userFolder,
+               private ?IUserSession $userSession,
+               private ITrashManager $trashManager,
+               private IFilesMetadataManager $filesMetadataManager,
+       ) {
+       }
+
+       public function handle(Event $event): void {
+               if ($this->userFolder === null || $this->userSession === null) {
+                       return;
+               }
+
+               $peerFile = null;
+
+               if ($event instanceof BeforeNodeRenamedEvent) {
+                       $peerFile = $this->getLivePhotoPeer($event->getSource()->getId());
+               } elseif ($event instanceof BeforeNodeRestoredEvent) {
+                       $peerFile = $this->getLivePhotoPeer($event->getSource()->getId());
+               } elseif ($event instanceof BeforeNodeDeletedEvent) {
+                       $peerFile = $this->getLivePhotoPeer($event->getNode()->getId());
+               } elseif ($event instanceof CacheEntryRemovedEvent) {
+                       $peerFile = $this->getLivePhotoPeer($event->getFileId());
+               } else {
+                       return;
+               }
+
+               if ($peerFile === null) {
+                       return;
+               }
+
+               if ($event instanceof BeforeNodeRenamedEvent) {
+                       $sourceFile = $event->getSource();
+                       $targetFile = $event->getTarget();
+                       $targetParent = $targetFile->getParent();
+                       $sourceExtension = $sourceFile->getExtension();
+                       $peerFileExtension = $peerFile->getExtension();
+                       $targetName = $targetFile->getName();
+                       $targetPath = $targetFile->getPath();
+
+                       // Prevent rename of the .mov file if the peer file do not have the same path.
+                       if ($sourceFile->getMimetype() === 'video/quicktime') {
+                               $peerFilePath = $this->pendingRenames[$peerFile->getId()] ?? $peerFile->getPath();
+                               $targetPathWithoutExtension = preg_replace("/\.$sourceExtension$/", '', $targetPath);
+                               $peerFilePathWithoutExtension = preg_replace("/\.$peerFileExtension$/", '', $peerFilePath);
+
+                               if ($targetPathWithoutExtension !== $peerFilePathWithoutExtension) {
+                                       $event->abortOperation(new NotPermittedException("The video part of a live photo need to have the same name as the image"));
+                               }
+
+                               unset($this->pendingRenames[$peerFile->getId()]);
+                               return;
+                       }
+
+                       if (!str_ends_with($targetName, ".".$sourceExtension)) {
+                               $event->abortOperation(new NotPermittedException("Cannot change the extension of a live photo"));
+                       }
+
+                       try {
+                               $targetParent->get($targetName);
+                               $event->abortOperation(new NotPermittedException("A file already exist at destination path"));
+                       } catch (NotFoundException $ex) {
+                       }
+                       try {
+                               $peerTargetName = preg_replace("/\.$sourceExtension$/", '.mov', $targetName);
+                               $targetParent->get($peerTargetName);
+                               $event->abortOperation(new NotPermittedException("A file already exist at destination path"));
+                       } catch (NotFoundException $ex) {
+                       }
+
+                       $peerTargetPath = preg_replace("/\.$sourceExtension$/", '.mov', $targetPath);
+                       $this->pendingRenames[$sourceFile->getId()] = $targetPath;
+                       try {
+                               $peerFile->move($peerTargetPath);
+                       } catch (\Throwable $ex) {
+                               $event->abortOperation($ex);
+                       }
+                       return;
+               }
+
+               if ($event instanceof BeforeNodeDeletedEvent) {
+                       $deletedFile = $event->getNode();
+                       if ($deletedFile->getMimetype() === 'video/quicktime') {
+                               if (isset($this->pendingDeletion[$peerFile->getId()])) {
+                                       unset($this->pendingDeletion[$peerFile->getId()]);
+                                       return;
+                               } else {
+                                       $event->abortOperation(new NotPermittedException("Cannot delete the video part of a live photo"));
+                               }
+                       } else {
+                               $this->pendingDeletion[$deletedFile->getId()] = true;
+                               try {
+                                       $peerFile->delete();
+                               } catch (\Throwable $ex) {
+                                       $event->abortOperation($ex);
+                               }
+                       }
+                       return;
+               }
+
+               if ($event instanceof CacheEntryRemovedEvent) {
+                       $peerFile->delete();
+               }
+
+               if ($event instanceof BeforeNodeRestoredEvent) {
+                       $sourceFile = $event->getSource();
+
+                       if ($sourceFile->getMimetype() === 'video/quicktime') {
+                               if (isset($this->pendingRestores[$peerFile->getId()])) {
+                                       unset($this->pendingRestores[$peerFile->getId()]);
+                                       return;
+                               } else {
+                                       $event->abortOperation(new NotPermittedException("Cannot restore the video part of a live photo"));
+                               }
+                       } else {
+                               $user = $this->userSession->getUser();
+                               if ($user === null) {
+                                       return;
+                               }
+
+                               $peerTrashItem = $this->trashManager->getTrashNodeById($user, $peerFile->getId());
+
+                               // Peer file in not in the bin, no need to restore it.
+                               if ($peerTrashItem === null) {
+                                       return;
+                               }
+
+                               $trashRoot = $this->trashManager->listTrashRoot($user);
+                               $trashItem = $this->getTrashItem($trashRoot, $peerFile->getInternalPath());
+
+                               if ($trashItem === null) {
+                                       $event->abortOperation(new NotFoundException("Couldn't find peer file in trashbin"));
+                               }
+
+                               $this->pendingRestores[$sourceFile->getId()] = true;
+                               try {
+                                       $this->trashManager->restoreItem($trashItem);
+                               } catch (\Throwable $ex) {
+                                       $event->abortOperation($ex);
+                               }
+                       }
+               }
+       }
+
+       private function getLivePhotoPeer(int $nodeId): ?Node {
+               if ($this->userFolder === null || $this->userSession === null) {
+                       return null;
+               }
+
+               try {
+                       $metadata = $this->filesMetadataManager->getMetadata($nodeId);
+               } catch (FilesMetadataNotFoundException $ex) {
+                       return null;
+               }
+
+               if (!$metadata->hasKey('files-live-photo')) {
+                       return null;
+               }
+
+               $peerFileId = (int)$metadata->getString('files-live-photo');
+
+               // Check the user's folder.
+               $nodes = $this->userFolder->getById($peerFileId);
+               if (count($nodes) !== 0) {
+                       return $nodes[0];
+               }
+
+               // Check the user's trashbin.
+               $user = $this->userSession->getUser();
+               if ($user !== null) {
+                       $peerFile = $this->trashManager->getTrashNodeById($user, $peerFileId);
+                       if ($peerFile !== null) {
+                               return $peerFile;
+                       }
+               }
+
+               $metadata->unset('files-live-photo');
+               return null;
+       }
+
+       private function getTrashItem(array $trashFolder, string $path): ?ITrashItem {
+               foreach($trashFolder as $trashItem) {
+                       if (str_starts_with($path, "files_trashbin/files".$trashItem->getTrashPath())) {
+                               if ($path === "files_trashbin/files".$trashItem->getTrashPath()) {
+                                       return $trashItem;
+                               }
+
+                               if ($trashItem instanceof Folder) {
+                                       $node = $this->getTrashItem($trashItem->getDirectoryListing(), $path);
+                                       if ($node !== null) {
+                                               return $node;
+                                       }
+                               }
+                       }
+               }
+
+               return null;
+       }
+}
index 88678d8a72abdeb69d259435e0452c5a881b89ca..c243d1d291cbb6010627d7f5409acf55d5d6aa79 100644 (file)
@@ -16,7 +16,9 @@ return array(
     'OCA\\Files_Trashbin\\Command\\RestoreAllFiles' => $baseDir . '/../lib/Command/RestoreAllFiles.php',
     'OCA\\Files_Trashbin\\Command\\Size' => $baseDir . '/../lib/Command/Size.php',
     'OCA\\Files_Trashbin\\Controller\\PreviewController' => $baseDir . '/../lib/Controller/PreviewController.php',
+    'OCA\\Files_Trashbin\\Events\\BeforeNodeRestoredEvent' => $baseDir . '/../lib/Events/BeforeNodeRestoredEvent.php',
     'OCA\\Files_Trashbin\\Events\\MoveToTrashEvent' => $baseDir . '/../lib/Events/MoveToTrashEvent.php',
+    'OCA\\Files_Trashbin\\Events\\NodeRestoredEvent' => $baseDir . '/../lib/Events/NodeRestoredEvent.php',
     'OCA\\Files_Trashbin\\Exceptions\\CopyRecursiveException' => $baseDir . '/../lib/Exceptions/CopyRecursiveException.php',
     'OCA\\Files_Trashbin\\Expiration' => $baseDir . '/../lib/Expiration.php',
     'OCA\\Files_Trashbin\\Helper' => $baseDir . '/../lib/Helper.php',
index 440dc3ee566d47a1de3fff0b27745033bb205c21..ffa3916460b02a9f39a21f51c2d91167c264f89d 100644 (file)
@@ -31,7 +31,9 @@ class ComposerStaticInitFiles_Trashbin
         'OCA\\Files_Trashbin\\Command\\RestoreAllFiles' => __DIR__ . '/..' . '/../lib/Command/RestoreAllFiles.php',
         'OCA\\Files_Trashbin\\Command\\Size' => __DIR__ . '/..' . '/../lib/Command/Size.php',
         'OCA\\Files_Trashbin\\Controller\\PreviewController' => __DIR__ . '/..' . '/../lib/Controller/PreviewController.php',
+        'OCA\\Files_Trashbin\\Events\\BeforeNodeRestoredEvent' => __DIR__ . '/..' . '/../lib/Events/BeforeNodeRestoredEvent.php',
         'OCA\\Files_Trashbin\\Events\\MoveToTrashEvent' => __DIR__ . '/..' . '/../lib/Events/MoveToTrashEvent.php',
+        'OCA\\Files_Trashbin\\Events\\NodeRestoredEvent' => __DIR__ . '/..' . '/../lib/Events/NodeRestoredEvent.php',
         'OCA\\Files_Trashbin\\Exceptions\\CopyRecursiveException' => __DIR__ . '/..' . '/../lib/Exceptions/CopyRecursiveException.php',
         'OCA\\Files_Trashbin\\Expiration' => __DIR__ . '/..' . '/../lib/Expiration.php',
         'OCA\\Files_Trashbin\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php',
diff --git a/apps/files_trashbin/lib/Events/BeforeNodeRestoredEvent.php b/apps/files_trashbin/lib/Events/BeforeNodeRestoredEvent.php
new file mode 100644 (file)
index 0000000..9c1bf4e
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Files_Trashbin\Events;
+
+use Exception;
+use OCP\Files\Events\Node\AbstractNodesEvent;
+use OCP\Files\Node;
+
+/**
+ * @since 28.0.0
+ */
+class BeforeNodeRestoredEvent extends AbstractNodesEvent {
+       public function __construct(Node $source, Node $target, private bool &$run) {
+               parent::__construct($source, $target);
+       }
+
+       /**
+        * @return never
+        */
+       public function abortOperation(\Throwable $ex = null) {
+               $this->stopPropagation();
+               $this->run = false;
+               if ($ex !== null) {
+                       throw $ex;
+               } else {
+                       throw new Exception('Operation aborted');
+               }
+       }
+}
diff --git a/apps/files_trashbin/lib/Events/NodeRestoredEvent.php b/apps/files_trashbin/lib/Events/NodeRestoredEvent.php
new file mode 100644 (file)
index 0000000..2f73fff
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * @copyright Copyright (c) 2023 Louis Chemineau <louis@chmn.me>
+ *
+ * @author Louis Chemineau <louis@chmn.me>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+namespace OCA\Files_Trashbin\Events;
+
+use OCP\Files\Events\Node\AbstractNodesEvent;
+use OCP\Files\Node;
+
+/**
+ * @since 28.0.0
+ */
+class NodeRestoredEvent extends AbstractNodesEvent {
+       public function __construct(Node $source, Node $target) {
+               parent::__construct($source, $target);
+       }
+}
index 051665acde1f11c4b0d587139c8390296f79500b..442abc13670c6fbaf354f17aad6a17fc880a38d3 100644 (file)
  */
 namespace OCA\Files_Trashbin;
 
+use Exception;
 use OC\Files\Cache\Cache;
 use OC\Files\Cache\CacheEntry;
 use OC\Files\Cache\CacheQueryBuilder;
 use OC\Files\Filesystem;
+use OC\Files\Node\File;
+use OC\Files\Node\Folder;
+use OC\Files\Node\NonExistingFile;
+use OC\Files\Node\NonExistingFolder;
 use OC\Files\ObjectStore\ObjectStoreStorage;
 use OC\Files\View;
 use OC_User;
 use OCA\Files_Trashbin\AppInfo\Application;
 use OCA\Files_Trashbin\Command\Expire;
+use OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent;
+use OCA\Files_Trashbin\Events\NodeRestoredEvent;
 use OCP\App\IAppManager;
 use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\Files\File;
-use OCP\Files\Folder;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
 use OCP\Files\NotFoundException;
 use OCP\Files\NotPermittedException;
 use OCP\FilesMetadata\IFilesMetadataManager;
@@ -508,6 +516,21 @@ class Trashbin {
                if (!$view->isCreatable(dirname($target))) {
                        throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
                }
+
+               $sourcePath = Filesystem::normalizePath($file);
+               $targetPath = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
+
+               $sourceNode = self::getNodeForPath($sourcePath);
+               $targetNode = self::getNodeForPath($targetPath);
+               $run = true;
+               $event = new BeforeNodeRestoredEvent($sourceNode, $targetNode, $run);
+               $dispatcher = \OC::$server->get(IEventDispatcher::class);
+               $dispatcher->dispatchTyped($event);
+
+               if (!$run) {
+                       return false;
+               }
+
                $restoreResult = $view->rename($source, $target);
 
                // handle the restore result
@@ -516,8 +539,13 @@ class Trashbin {
                        $view->chroot('/' . $user . '/files');
                        $view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
                        $view->chroot($fakeRoot);
-                       \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
-                               'trashPath' => Filesystem::normalizePath($file)]);
+                       \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => $targetPath, 'trashPath' => $sourcePath]);
+
+                       $sourceNode = self::getNodeForPath($sourcePath);
+                       $targetNode = self::getNodeForPath($targetPath);
+                       $event = new NodeRestoredEvent($sourceNode, $targetNode);
+                       $dispatcher = \OC::$server->get(IEventDispatcher::class);
+                       $dispatcher->dispatchTyped($event);
 
                        self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
 
@@ -1151,4 +1179,33 @@ class Trashbin {
                }
                return $trashFilename;
        }
+
+       private static function getNodeForPath(string $path): Node {
+               $user = OC_User::getUser();
+               $rootFolder = \OC::$server->get(IRootFolder::class);
+
+               if ($user !== false) {
+                       $userFolder = $rootFolder->getUserFolder($user);
+                       /** @var Folder */
+                       $trashFolder = $userFolder->getParent()->get('files_trashbin/files');
+                       try {
+                               return $trashFolder->get($path);
+                       } catch (NotFoundException $ex) {
+                       }
+               }
+
+               $view = \OC::$server->get(View::class);
+               $fsView = Filesystem::getView();
+               if ($fsView === null) {
+                       throw new Exception('View should not be null');
+               }
+
+               $fullPath = $fsView->getAbsolutePath($path);
+
+               if (Filesystem::is_dir($path)) {
+                       return new NonExistingFolder($rootFolder, $view, $fullPath);
+               } else {
+                       return new NonExistingFile($rootFolder, $view, $fullPath);
+               }
+       }
 }
index a8e76d95c22c41e8be4e04248f483201b706fd97..f61eedee66e5526bad21552015028eb1225d1139 100644 (file)
@@ -133,7 +133,7 @@ class HookConnector {
                $this->root->emit('\OC\Files', 'preDelete', [$node]);
                $this->dispatcher->dispatch('\OCP\Files::preDelete', new GenericEvent($node));
 
-               $event = new BeforeNodeDeletedEvent($node);
+               $event = new BeforeNodeDeletedEvent($node, $arguments['run']);
                $this->dispatcher->dispatchTyped($event);
        }
 
@@ -171,7 +171,7 @@ 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);
+               $event = new BeforeNodeRenamedEvent($source, $target, $arguments['run']);
                $this->dispatcher->dispatchTyped($event);
        }
 
index 316a30fadd83bb43bd1289642129d78211da49e6..dd29a39a27994d3f970a59aa2afde162ee7e7ba1 100644 (file)
@@ -25,8 +25,31 @@ declare(strict_types=1);
  */
 namespace OCP\Files\Events\Node;
 
+use Exception;
+use OCP\Files\Node;
+
 /**
  * @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
+        */
+       public function abortOperation(\Throwable $ex = null) {
+               $this->stopPropagation();
+               $this->run = false;
+               if ($ex !== null) {
+                       throw $ex;
+               } else {
+                       throw new Exception('Operation aborted');
+               }
+       }
 }
index efbef03e38387927a18d027ea17387777f3eed72..c6876666713e14512f7e705f2e7f5cc1e24c99b6 100644 (file)
@@ -25,8 +25,31 @@ declare(strict_types=1);
  */
 namespace OCP\Files\Events\Node;
 
+use Exception;
+use OCP\Files\Node;
+
 /**
  * @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
+        */
+       public function abortOperation(\Throwable $ex = null) {
+               $this->stopPropagation();
+               $this->run = false;
+               if ($ex !== null) {
+                       throw $ex;
+               } else {
+                       throw new Exception('Operation aborted');
+               }
+       }
 }