123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- <?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 array $pendingRenames = [];
- /** @var Array<int, bool> */
- private array $pendingDeletion = [];
- /** @var Array<int, bool> */
- private array $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());
- }
-
- if ($peerFile === null) {
- return; // not a Live Photo
- }
-
- if ($event instanceof BeforeNodeRenamedEvent) {
- $this->handleMove($event, $peerFile);
- } elseif ($event instanceof BeforeNodeDeletedEvent) {
- $this->handleDeletion($event, $peerFile);
- } elseif ($event instanceof CacheEntryRemovedEvent) {
- $peerFile->delete();
- } elseif ($event instanceof BeforeNodeRestoredEvent) {
- $this->handleRestore($event, $peerFile);
- }
- }
-
- /**
- * During rename events, which also include move operations,
- * we rename the peer file using the same name.
- * The event listener being singleton, we can store the current state
- * of pending renames inside the 'pendingRenames' property,
- * to prevent infinite recursive.
- */
- private function handleMove(BeforeNodeRenamedEvent $event, Node $peerFile): void {
- $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"));
- }
-
- try {
- $targetParent->get($targetName);
- $event->abortOperation(new NotPermittedException("A file already exist at destination path of the Live Photo"));
- } catch (NotFoundException $ex) {
- }
-
- $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) {
- }
-
- // in case the rename was initiated from this listener, we stop right now
- if (array_key_exists($peerFile->getId(), $this->pendingRenames)) {
- return;
- }
-
- $this->pendingRenames[$sourceFile->getId()] = $targetPath;
- try {
- $peerFile->move($targetParent->getPath() . '/' . $peerTargetName);
- } catch (\Throwable $ex) {
- $event->abortOperation($ex);
- }
- unset($this->pendingRenames[$sourceFile->getId()]);
- }
-
- /**
- * During deletion event, we trigger another recursive delete on the peer file.
- * Delete operations on the .mov file directly are currently blocked.
- * The event listener being singleton, we can store the current state
- * of pending deletions inside the 'pendingDeletions' property,
- * to prevent infinite recursivity.
- */
- private function handleDeletion(BeforeNodeDeletedEvent $event, Node $peerFile): void {
- $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;
- }
-
- /**
- * During restore event, we trigger another recursive restore on the peer file.
- * Restore operations on the .mov file directly are currently blocked.
- * The event listener being singleton, we can store the current state
- * of pending restores inside the 'pendingRestores' property,
- * to prevent infinite recursivity.
- */
- private function handleRestore(BeforeNodeRestoredEvent $event, Node $peerFile): void {
- $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 is 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);
- }
- }
- }
-
- /**
- * Helper method to get the associated live photo file.
- * We first look for it in the user folder, and if we
- * cannot find it here, we look for it in the user's trashbin.
- */
- 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.
- $node = $this->userFolder->getFirstNodeById($peerFileId);
- if ($node) {
- return $node;
- }
-
- // 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;
- }
-
- /**
- * There is currently no method to restore a file based on its fileId or path.
- * So we have to manually find a ITrashItem from the trash item list.
- * TODO: This should be replaced by a proper method in the TrashManager.
- */
- 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;
- }
- }
|