aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/lib
diff options
context:
space:
mode:
authorLouis Chemineau <louis@chmn.me>2023-11-29 19:05:35 +0100
committerLouis Chemineau <louis@chmn.me>2023-11-29 19:07:32 +0100
commit2f6a4bf4a157accf923db341f3b2836a3aead3c9 (patch)
treefd6b106dbfa24d1486ca3bb6ca291d937c46c4c4 /apps/files/lib
parentb213fc7c74e938358b2257e87ee5754b0d71ad4a (diff)
downloadnextcloud-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.php9
-rw-r--r--apps/files/lib/Listener/SyncLivePhotosListener.php252
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;
+ }
+}