Skip to content

Commit

Permalink
Synchronize operation on live photo files
Browse files Browse the repository at this point in the history
Signed-off-by: Louis Chemineau <louis@chmn.me>
  • Loading branch information
artonge authored and backportbot-nextcloud[bot] committed Nov 30, 2023
1 parent a1434af commit 86d88f6
Show file tree
Hide file tree
Showing 13 changed files with 468 additions and 8 deletions.
1 change: 1 addition & 0 deletions apps/files/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
'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',
Expand Down
1 change: 1 addition & 0 deletions apps/files/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions apps/files/composer/composer/installed.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
'reference' => 'ba1af2b22e5409c62ea2bdf7eb0e13c282ed70e8',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
Expand All @@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
'reference' => 'ba1af2b22e5409c62ea2bdf7eb0e13c282ed70e8',
'type' => 'library',
'install_path' => __DIR__ . '/../',
'aliases' => array(),
Expand Down
9 changes: 9 additions & 0 deletions apps/files/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@
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;
Expand All @@ -56,6 +58,9 @@
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;
Expand Down Expand Up @@ -120,6 +125,10 @@ public function register(IRegistrationContext $context): void {

$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);

Expand Down
252 changes: 252 additions & 0 deletions apps/files/lib/Listener/SyncLivePhotosListener.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions apps/files_trashbin/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
'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',
Expand Down
2 changes: 2 additions & 0 deletions apps/files_trashbin/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
52 changes: 52 additions & 0 deletions apps/files_trashbin/lib/Events/BeforeNodeRestoredEvent.php
Original file line number Diff line number Diff line change
@@ -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');
}
}
}

0 comments on commit 86d88f6

Please sign in to comment.