diff options
author | Louis Chemineau <louis@chmn.me> | 2023-11-29 19:05:35 +0100 |
---|---|---|
committer | Louis Chemineau <louis@chmn.me> | 2023-11-29 19:07:32 +0100 |
commit | 2f6a4bf4a157accf923db341f3b2836a3aead3c9 (patch) | |
tree | fd6b106dbfa24d1486ca3bb6ca291d937c46c4c4 /apps/files/lib | |
parent | b213fc7c74e938358b2257e87ee5754b0d71ad4a (diff) | |
download | nextcloud-server-2f6a4bf4a157accf923db341f3b2836a3aead3c9.tar.gz nextcloud-server-2f6a4bf4a157accf923db341f3b2836a3aead3c9.zip |
Synchronize operation on live photo files
Signed-off-by: Louis Chemineau <louis@chmn.me>
Diffstat (limited to 'apps/files/lib')
-rw-r--r-- | apps/files/lib/AppInfo/Application.php | 9 | ||||
-rw-r--r-- | apps/files/lib/Listener/SyncLivePhotosListener.php | 252 |
2 files changed, 261 insertions, 0 deletions
diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 5934bc1c7ce..41423a65ca7 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -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 index 00000000000..558b9e8eed6 --- /dev/null +++ b/apps/files/lib/Listener/SyncLivePhotosListener.php @@ -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; + } +} |