diff --git a/apps/files/composer/composer/autoload_classmap.php b/apps/files/composer/composer/autoload_classmap.php index 49763bf28f819..bc2ee4970ba42 100644 --- a/apps/files/composer/composer/autoload_classmap.php +++ b/apps/files/composer/composer/autoload_classmap.php @@ -69,6 +69,7 @@ 'OCA\\Files\\Search\\FilesSearchProvider' => $baseDir . '/../lib/Search/FilesSearchProvider.php', 'OCA\\Files\\Service\\DirectEditingService' => $baseDir . '/../lib/Service/DirectEditingService.php', 'OCA\\Files\\Service\\OwnershipTransferService' => $baseDir . '/../lib/Service/OwnershipTransferService.php', + 'OCA\\Files\\Service\\SyncLivePhotosService' => $baseDir . '/../lib/Service/SyncLivePhotosService.php', 'OCA\\Files\\Service\\TagService' => $baseDir . '/../lib/Service/TagService.php', 'OCA\\Files\\Service\\UserConfig' => $baseDir . '/../lib/Service/UserConfig.php', 'OCA\\Files\\Service\\ViewConfig' => $baseDir . '/../lib/Service/ViewConfig.php', diff --git a/apps/files/composer/composer/autoload_static.php b/apps/files/composer/composer/autoload_static.php index 5a44f49a59900..c77c0d0a637fa 100644 --- a/apps/files/composer/composer/autoload_static.php +++ b/apps/files/composer/composer/autoload_static.php @@ -84,6 +84,7 @@ class ComposerStaticInitFiles 'OCA\\Files\\Search\\FilesSearchProvider' => __DIR__ . '/..' . '/../lib/Search/FilesSearchProvider.php', 'OCA\\Files\\Service\\DirectEditingService' => __DIR__ . '/..' . '/../lib/Service/DirectEditingService.php', 'OCA\\Files\\Service\\OwnershipTransferService' => __DIR__ . '/..' . '/../lib/Service/OwnershipTransferService.php', + 'OCA\\Files\\Service\\SyncLivePhotosService' => __DIR__ . '/..' . '/../lib/Service/SyncLivePhotosService.php', 'OCA\\Files\\Service\\TagService' => __DIR__ . '/..' . '/../lib/Service/TagService.php', 'OCA\\Files\\Service\\UserConfig' => __DIR__ . '/..' . '/../lib/Service/UserConfig.php', 'OCA\\Files\\Service\\ViewConfig' => __DIR__ . '/..' . '/../lib/Service/ViewConfig.php', diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index c096d8a757a67..d4330f759d37e 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -51,7 +51,6 @@ 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; @@ -131,11 +130,10 @@ public function register(IRegistrationContext $context): void { $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->registerEventListener(LoadSearchPlugins::class, LoadSearchPluginsListener::class); $context->registerEventListener(BeforeNodeCopiedEvent::class, SyncLivePhotosListener::class); $context->registerEventListener(NodeCopiedEvent::class, SyncLivePhotosListener::class); + $context->registerEventListener(LoadSearchPlugins::class, LoadSearchPluginsListener::class); $context->registerSearchProvider(FilesSearchProvider::class); diff --git a/apps/files/lib/Listener/SyncLivePhotosListener.php b/apps/files/lib/Listener/SyncLivePhotosListener.php index 8f8234428d680..64c174ea7b0d8 100644 --- a/apps/files/lib/Listener/SyncLivePhotosListener.php +++ b/apps/files/lib/Listener/SyncLivePhotosListener.php @@ -24,9 +24,7 @@ namespace OCA\Files\Listener; -use OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent; -use OCA\Files_Trashbin\Trash\ITrashItem; -use OCA\Files_Trashbin\Trash\ITrashManager; +use OCA\Files\Service\SyncLivePhotosService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Exceptions\AbortedEventException; @@ -39,10 +37,7 @@ 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 @@ -52,37 +47,40 @@ class SyncLivePhotosListener implements IEventListener { private array $pendingRenames = []; /** @var Array */ private array $pendingDeletion = []; - /** @var Array */ - private array $pendingRestores = []; public function __construct( private ?Folder $userFolder, - private ?IUserSession $userSession, - private ITrashManager $trashManager, private IFilesMetadataManager $filesMetadataManager, + private SyncLivePhotosService $syncLivePhotosService, ) { } public function handle(Event $event): void { - if ($this->userFolder === null || $this->userSession === null) { + if ($this->userFolder === null) { return; } - $peerFile = null; + $peerFileId = null; + if ($event instanceof BeforeNodeRenamedEvent) { - $peerFile = $this->getLivePhotoPeer($event->getSource()->getId()); - } elseif ($event instanceof BeforeNodeRestoredEvent) { - $peerFile = $this->getLivePhotoPeer($event->getSource()->getId()); + $peerFileId = $this->syncLivePhotosService->getLivePhotoPeerId($event->getSource()->getId()); } elseif ($event instanceof BeforeNodeDeletedEvent) { - $peerFile = $this->getLivePhotoPeer($event->getNode()->getId()); + $peerFileId = $this->syncLivePhotosService->getLivePhotoPeerId($event->getNode()->getId()); } elseif ($event instanceof CacheEntryRemovedEvent) { - $peerFile = $this->getLivePhotoPeer($event->getFileId()); + $peerFileId = $this->syncLivePhotosService->getLivePhotoPeerId($event->getFileId()); } elseif ($event instanceof BeforeNodeCopiedEvent || $event instanceof NodeCopiedEvent) { - $peerFile = $this->getLivePhotoPeer($event->getSource()->getId()); + $peerFileId = $this->syncLivePhotosService->getLivePhotoPeerId($event->getSource()->getId()); + } + + if ($peerFileId === null) { + return; // Not a live photo. } + // Check the user's folder. + $peerFile = $this->userFolder->getFirstNodeById($peerFileId); + if ($peerFile === null) { - return; // not a Live Photo + return; // Peer file not found. } if ($event instanceof BeforeNodeRenamedEvent) { @@ -91,8 +89,6 @@ public function handle(Event $event): void { $this->handleDeletion($event, $peerFile); } elseif ($event instanceof CacheEntryRemovedEvent) { $peerFile->delete(); - } elseif ($event instanceof BeforeNodeRestoredEvent) { - $this->handleRestore($event, $peerFile); } elseif ($event instanceof BeforeNodeCopiedEvent) { $this->handleMove($event, $peerFile, true); } elseif ($event instanceof NodeCopiedEvent) { @@ -208,114 +204,4 @@ private function handleDeletion(BeforeNodeDeletedEvent $event, Node $peerFile): } 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; - } } diff --git a/apps/files/lib/Service/SyncLivePhotosService.php b/apps/files/lib/Service/SyncLivePhotosService.php new file mode 100644 index 0000000000000..19e50bcad085a --- /dev/null +++ b/apps/files/lib/Service/SyncLivePhotosService.php @@ -0,0 +1,52 @@ + + * + * @author Louis Chemineau + * + * @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 . + */ + +namespace OCA\Files\Service; + +use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; +use OCP\FilesMetadata\IFilesMetadataManager; + +class SyncLivePhotosService { + public function __construct( + private IFilesMetadataManager $filesMetadataManager, + ) { + } + + /** + * Get the associated live photo for a given file id + */ + public function getLivePhotoPeerId(int $fileId): ?int { + try { + $metadata = $this->filesMetadataManager->getMetadata($fileId); + } catch (FilesMetadataNotFoundException $ex) { + return null; + } + + if (!$metadata->hasKey('files-live-photo')) { + return null; + } + + return (int)$metadata->getString('files-live-photo'); + } +} diff --git a/apps/files/src/views/Settings.vue b/apps/files/src/views/Settings.vue index cefbdab4eec70..6005975f034e0 100644 --- a/apps/files/src/views/Settings.vue +++ b/apps/files/src/views/Settings.vue @@ -26,23 +26,28 @@ @update:open="onClose"> - {{ t('files', 'Sort favorites first') }} - {{ t('files', 'Sort folders before files') }} - {{ t('files', 'Show hidden files') }} - {{ t('files', 'Crop image previews') }} {{ t('files', 'Enable the grid view') }} diff --git a/apps/files_trashbin/composer/composer/autoload_classmap.php b/apps/files_trashbin/composer/composer/autoload_classmap.php index c243d1d291cbb..f1bc38f14924b 100644 --- a/apps/files_trashbin/composer/composer/autoload_classmap.php +++ b/apps/files_trashbin/composer/composer/autoload_classmap.php @@ -24,6 +24,7 @@ 'OCA\\Files_Trashbin\\Helper' => $baseDir . '/../lib/Helper.php', 'OCA\\Files_Trashbin\\Hooks' => $baseDir . '/../lib/Hooks.php', 'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => $baseDir . '/../lib/Listeners/LoadAdditionalScripts.php', + 'OCA\\Files_Trashbin\\Listeners\\SyncLivePhotosListener' => $baseDir . '/../lib/Listeners/SyncLivePhotosListener.php', 'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => $baseDir . '/../lib/Migration/Version1010Date20200630192639.php', 'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => $baseDir . '/../lib/Sabre/AbstractTrash.php', 'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => $baseDir . '/../lib/Sabre/AbstractTrashFile.php', diff --git a/apps/files_trashbin/composer/composer/autoload_static.php b/apps/files_trashbin/composer/composer/autoload_static.php index ffa3916460b02..4c4b33574afa5 100644 --- a/apps/files_trashbin/composer/composer/autoload_static.php +++ b/apps/files_trashbin/composer/composer/autoload_static.php @@ -39,6 +39,7 @@ class ComposerStaticInitFiles_Trashbin 'OCA\\Files_Trashbin\\Helper' => __DIR__ . '/..' . '/../lib/Helper.php', 'OCA\\Files_Trashbin\\Hooks' => __DIR__ . '/..' . '/../lib/Hooks.php', 'OCA\\Files_Trashbin\\Listeners\\LoadAdditionalScripts' => __DIR__ . '/..' . '/../lib/Listeners/LoadAdditionalScripts.php', + 'OCA\\Files_Trashbin\\Listeners\\SyncLivePhotosListener' => __DIR__ . '/..' . '/../lib/Listeners/SyncLivePhotosListener.php', 'OCA\\Files_Trashbin\\Migration\\Version1010Date20200630192639' => __DIR__ . '/..' . '/../lib/Migration/Version1010Date20200630192639.php', 'OCA\\Files_Trashbin\\Sabre\\AbstractTrash' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrash.php', 'OCA\\Files_Trashbin\\Sabre\\AbstractTrashFile' => __DIR__ . '/..' . '/../lib/Sabre/AbstractTrashFile.php', diff --git a/apps/files_trashbin/lib/AppInfo/Application.php b/apps/files_trashbin/lib/AppInfo/Application.php index 0f36fe37d29f8..e12a6ca8af94a 100644 --- a/apps/files_trashbin/lib/AppInfo/Application.php +++ b/apps/files_trashbin/lib/AppInfo/Application.php @@ -28,8 +28,10 @@ use OCA\DAV\Connector\Sabre\Principal; use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Files_Trashbin\Capabilities; +use OCA\Files_Trashbin\Events\BeforeNodeRestoredEvent; use OCA\Files_Trashbin\Expiration; use OCA\Files_Trashbin\Listeners\LoadAdditionalScripts; +use OCA\Files_Trashbin\Listeners\SyncLivePhotosListener; use OCA\Files_Trashbin\Trash\ITrashManager; use OCA\Files_Trashbin\Trash\TrashManager; use OCA\Files_Trashbin\UserMigration\TrashbinMigrator; @@ -62,6 +64,8 @@ public function register(IRegistrationContext $context): void { LoadAdditionalScriptsEvent::class, LoadAdditionalScripts::class ); + + $context->registerEventListener(BeforeNodeRestoredEvent::class, SyncLivePhotosListener::class); } public function boot(IBootContext $context): void { diff --git a/apps/files_trashbin/lib/Listeners/SyncLivePhotosListener.php b/apps/files_trashbin/lib/Listeners/SyncLivePhotosListener.php new file mode 100644 index 0000000000000..579b1864f0818 --- /dev/null +++ b/apps/files_trashbin/lib/Listeners/SyncLivePhotosListener.php @@ -0,0 +1,148 @@ + + * + * @author Louis Chemineau + * + * @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 . + */ + +namespace OCA\Files_Trashbin\Listeners; + +use OCA\Files\Service\SyncLivePhotosService; +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\Folder; +use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IUserSession; + +/** + * @template-implements IEventListener + */ +class SyncLivePhotosListener implements IEventListener { + /** @var Array */ + private array $pendingRestores = []; + + public function __construct( + private ?IUserSession $userSession, + private ITrashManager $trashManager, + private SyncLivePhotosService $syncLivePhotosService, + ) { + } + + public function handle(Event $event): void { + if ($this->userSession === null) { + return; + } + + /** @var BeforeNodeRestoredEvent $event */ + $peerFileId = $this->syncLivePhotosService->getLivePhotoPeerId($event->getSource()->getId()); + + if ($peerFileId === null) { + return; // Not a live photo. + } + + // Check the user's trashbin. + $user = $this->userSession?->getUser(); + if ($user === null) { + return; + } + + $peerFile = $this->trashManager->getTrashNodeById($user, $peerFileId); + + if ($peerFile === null) { + return; // Peer file not found. + } + + $this->handleRestore($event, $peerFile); + } + + /** + * 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); + } + } + } + + /** + * 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; + } +} diff --git a/cypress/e2e/files/live_photos.cy.ts b/cypress/e2e/files/live_photos.cy.ts new file mode 100644 index 0000000000000..1e240d9f3b39a --- /dev/null +++ b/cypress/e2e/files/live_photos.cy.ts @@ -0,0 +1,169 @@ +/** + * @copyright Copyright (c) 2024 Louis Chmn + * + * @author Louis Chmn + * + * @license AGPL-3.0-or-later + * + * 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 . + * + */ + +import type { User } from '@nextcloud/cypress' +import { copyFile, getRowForFile, moveFile, triggerActionForFile } from './FilesUtils' + +describe('Files: Live photos', { testIsolation: true }, () => { + let currentUser: User + let randomFileName: string + let jpgFileId: string|undefined + let movFileId: string|undefined + + before(() => { + cy.createRandomUser().then((user) => { + currentUser = user + randomFileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10) + + cy.uploadContent(currentUser, new Blob(['jpg file'], { type: 'image/jpg' }), 'image/jpg', `/${randomFileName}.jpg`) + cy.uploadContent(currentUser, new Blob(['mov file'], { type: 'video/mov' }), 'video/mov', `/${randomFileName}.mov`) + + // Get the file IDs + cy.login(currentUser) + cy.visit('/apps/files') + getRowForFile(`${randomFileName}.jpg`).invoke('attr', 'data-cy-files-list-row-fileid').then(fileId => { jpgFileId = fileId }) + getRowForFile(`${randomFileName}.mov`).invoke('attr', 'data-cy-files-list-row-fileid').then(fileId => { movFileId = fileId }) + + let hostname: string + cy.url().then(url => { hostname = new URL(url).hostname }) + + // PROPPATCH both files to update their live photo metadata + ;[['mov', jpgFileId], ['jpg', movFileId]].forEach(([ext, fileId]) => { + cy.then(() => { + cy.logout() + cy.request({ + method: 'PROPPATCH', + url: `http://${hostname}/remote.php/dav/files/${currentUser.userId}/${randomFileName}.${ext}`, + auth: { user: currentUser.userId, pass: currentUser.password }, + body: ` + + + + ${fileId} + + + `, + }) + }) + }) + }) + }) + + beforeEach(() => { + cy.login(currentUser) + cy.visit('/apps/files') + }) + + it('Only renders the .jpg file', () => { + cy.get(`[data-cy-files-list-row-fileid="${jpgFileId}"]`).should('have.length', 1) + cy.get(`[data-cy-files-list-row-fileid="${movFileId}"]`).should('have.length', 0) + }) + + context("'Show hidden files' is enabled", () => { + before(() => { + cy.login(currentUser) + cy.visit('/apps/files') + cy.get('[data-cy-files-navigation-settings-button]').click() + // Force:true because the checkbox is hidden by the pretty UI. + cy.get('[data-cy-files-settings-setting="show_hidden"] input').check({ force: true }) + }) + + it("Shows both files when 'Show hidden files' is enabled", () => { + cy.get(`[data-cy-files-list-row-fileid="${jpgFileId}"]`).should('have.length', 1) + cy.get(`[data-cy-files-list-row-fileid="${movFileId}"]`).should('have.length', 1) + }) + + it('Copies both files when copying the .jpg', () => { + copyFile(`${randomFileName}.jpg`, '.') + cy.visit('/apps/files') + + cy.get(`[data-cy-files-list-row-fileid="${jpgFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.jpg`) + cy.get(`[data-cy-files-list-row-fileid="${movFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.mov`) + cy.get(`[data-cy-files-list-row-name="${randomFileName} (copy).jpg"]`).should('have.length', 1) + cy.get(`[data-cy-files-list-row-name="${randomFileName} (copy).mov"]`).should('have.length', 1) + + triggerActionForFile(`${randomFileName} (copy).jpg`, 'delete') + }) + + it('Blocks copy both files when copying the .mov', () => { + copyFile(`${randomFileName}.mov`, '.') + cy.visit('/apps/files') + + cy.get(`[data-cy-files-list-row-fileid="${jpgFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.jpg`) + cy.get(`[data-cy-files-list-row-fileid="${movFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.mov`) + cy.get(`[data-cy-files-list-row-name="${randomFileName} (copy).jpg"]`).should('have.length', 0) + cy.get(`[data-cy-files-list-row-name="${randomFileName} (copy).mov"]`).should('have.length', 0) + }) + + it('Moves files when moving the .jpg', () => { + moveFile(`${randomFileName}.mov`, `${randomFileName}_moved.mov`) + cy.visit('/apps/files') + + cy.get(`[data-cy-files-list-row-fileid="${jpgFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.jpg`) + cy.get(`[data-cy-files-list-row-fileid="${movFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}_moved.mov`) + + moveFile(`${randomFileName}_copie.mov`, `${randomFileName}.mov`) + }) + + it('Blocks moving files when moving the .mov', () => { + moveFile(`${randomFileName}.mov`, `${randomFileName}_moved.mov`) + cy.visit('/apps/files') + + cy.get(`[data-cy-files-list-row-fileid="${jpgFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.jpg`) + cy.get(`[data-cy-files-list-row-fileid="${movFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.mov`) + }) + + it('Deletes files when deleting the .jpg', () => { + copyFile(`${randomFileName}.jpg`, '.') + cy.visit('/apps/files') + triggerActionForFile(`${randomFileName} (copy).jpg`, 'delete') + cy.visit('/apps/files') + + cy.get(`[data-cy-files-list-row-fileid="${jpgFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.jpg`) + cy.get(`[data-cy-files-list-row-fileid="${movFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.mov`) + cy.get(`[data-cy-files-list-row-name="${randomFileName} (copy).jpg"]`).should('have.length', 0) + cy.get(`[data-cy-files-list-row-name="${randomFileName} (copy).mov"]`).should('have.length', 0) + }) + + it('Blocks deleting files when moving the .mov', () => { + copyFile(`${randomFileName}.jpg`, '.') + cy.visit('/apps/files') + triggerActionForFile(`${randomFileName} (copy).mov`, 'delete') + cy.visit('/apps/files') + + cy.get(`[data-cy-files-list-row-fileid="${jpgFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.jpg`) + cy.get(`[data-cy-files-list-row-fileid="${movFileId}"]`).invoke('attr', 'data-cy-files-list-row-name').should('equal', `${randomFileName}.mov`) + cy.get(`[data-cy-files-list-row-name="${randomFileName} (copy).jpg"]`).should('have.length', 1) + cy.get(`[data-cy-files-list-row-name="${randomFileName} (copy).mov"]`).should('have.length', 1) + + triggerActionForFile(`${randomFileName} (copy).jpg`, 'delete') + }) + + it('Restores files when restoring the .jpg', () => { + + }) + + it('Blocks restoring files when restoring the .mov', () => { + + }) + }) +}) diff --git a/cypress/e2e/files_sharing/filesSharingUtils.ts b/cypress/e2e/files_sharing/filesSharingUtils.ts index ee80041b61971..cb4071533805e 100644 --- a/cypress/e2e/files_sharing/filesSharingUtils.ts +++ b/cypress/e2e/files_sharing/filesSharingUtils.ts @@ -58,8 +58,10 @@ export function updateShare(fileName: string, index: number, shareSettings: Part if (shareSettings.download !== undefined) { cy.get('[data-cy-files-sharing-share-permissions-checkbox="download"]').find('input').as('downloadCheckbox') if (shareSettings.download) { + // Force:true because the checkbox is hidden by the pretty UI. cy.get('@downloadCheckbox').check({ force: true, scrollBehavior: 'nearest' }) } else { + // Force:true because the checkbox is hidden by the pretty UI. cy.get('@downloadCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' }) } } @@ -67,8 +69,10 @@ export function updateShare(fileName: string, index: number, shareSettings: Part if (shareSettings.read !== undefined) { cy.get('[data-cy-files-sharing-share-permissions-checkbox="read"]').find('input').as('readCheckbox') if (shareSettings.read) { + // Force:true because the checkbox is hidden by the pretty UI. cy.get('@readCheckbox').check({ force: true, scrollBehavior: 'nearest' }) } else { + // Force:true because the checkbox is hidden by the pretty UI. cy.get('@readCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' }) } } @@ -76,8 +80,10 @@ export function updateShare(fileName: string, index: number, shareSettings: Part if (shareSettings.update !== undefined) { cy.get('[data-cy-files-sharing-share-permissions-checkbox="update"]').find('input').as('updateCheckbox') if (shareSettings.update) { + // Force:true because the checkbox is hidden by the pretty UI. cy.get('@updateCheckbox').check({ force: true, scrollBehavior: 'nearest' }) } else { + // Force:true because the checkbox is hidden by the pretty UI. cy.get('@updateCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' }) } } @@ -85,8 +91,10 @@ export function updateShare(fileName: string, index: number, shareSettings: Part if (shareSettings.delete !== undefined) { cy.get('[data-cy-files-sharing-share-permissions-checkbox="delete"]').find('input').as('deleteCheckbox') if (shareSettings.delete) { + // Force:true because the checkbox is hidden by the pretty UI. cy.get('@deleteCheckbox').check({ force: true, scrollBehavior: 'nearest' }) } else { + // Force:true because the checkbox is hidden by the pretty UI. cy.get('@deleteCheckbox').uncheck({ force: true, scrollBehavior: 'nearest' }) } } diff --git a/cypress/e2e/files_versions/version_restoration.cy.ts b/cypress/e2e/files_versions/version_restoration.cy.ts index c5dbaeab96428..d9d983b0d097d 100644 --- a/cypress/e2e/files_versions/version_restoration.cy.ts +++ b/cypress/e2e/files_versions/version_restoration.cy.ts @@ -113,7 +113,7 @@ describe('Versions restoration', () => { auth: { user: recipient.userId, pass: recipient.password }, headers: { cookie: '', - Destination: 'https://nextcloud_server1.test/remote.php/dav/versions/admin/restore/target', + Destination: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/restore/target`, }, url: `http://${hostname}/remote.php/dav/versions/${recipient.userId}/versions/${fileId}/${versionId}`, failOnStatusCode: false,