aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLouis <louis@chmn.me>2024-12-04 17:02:56 +0100
committerGitHub <noreply@github.com>2024-12-04 17:02:56 +0100
commit1ef3e3e753aa1e1e767f6e1ed6576a0c00b27030 (patch)
tree2dad217ec9bacefec0813f81818f4da8d415588d
parent27331d48e377d5e908439cfdef8950b4b5ec47b2 (diff)
parent8be6a7c7dd51b018a20fb4f1c7e530999bc10faf (diff)
downloadnextcloud-server-1ef3e3e753aa1e1e767f6e1ed6576a0c00b27030.tar.gz
nextcloud-server-1ef3e3e753aa1e1e767f6e1ed6576a0c00b27030.zip
Merge pull request #49293 from nextcloud/artonge/fix/handle_folders_copy_live_photos
fix: Handle copy of folders containing live photos
-rw-r--r--apps/dav/lib/Connector/Sabre/Directory.php8
-rw-r--r--apps/files/lib/Listener/SyncLivePhotosListener.php184
-rw-r--r--apps/files_versions/lib/Listener/VersionStorageMoveListener.php3
-rw-r--r--cypress/e2e/files/FilesUtils.ts2
-rw-r--r--cypress/e2e/files/LivePhotosUtils.ts107
-rw-r--r--cypress/e2e/files/live_photos.cy.ts120
-rw-r--r--lib/private/Files/Node/HookConnector.php6
7 files changed, 289 insertions, 141 deletions
diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php
index a193417831c..ebef7f91ee0 100644
--- a/apps/dav/lib/Connector/Sabre/Directory.php
+++ b/apps/dav/lib/Connector/Sabre/Directory.php
@@ -447,7 +447,13 @@ class Directory extends Node implements \Sabre\DAV\ICollection, \Sabre\DAV\IQuot
throw new InvalidPath($ex->getMessage());
}
- return $this->fileView->copy($sourcePath, $destinationPath);
+ $copyOkay = $this->fileView->copy($sourcePath, $destinationPath);
+
+ if (!$copyOkay) {
+ throw new \Sabre\DAV\Exception\Forbidden('Copy did not proceed');
+ }
+
+ return true;
} catch (StorageNotAvailableException $e) {
throw new ServiceUnavailable($e->getMessage(), $e->getCode(), $e);
} catch (ForbiddenException $ex) {
diff --git a/apps/files/lib/Listener/SyncLivePhotosListener.php b/apps/files/lib/Listener/SyncLivePhotosListener.php
index 34789187644..6334e5d16a6 100644
--- a/apps/files/lib/Listener/SyncLivePhotosListener.php
+++ b/apps/files/lib/Listener/SyncLivePhotosListener.php
@@ -8,18 +8,23 @@ declare(strict_types=1);
namespace OCA\Files\Listener;
+use Exception;
+use OC\Files\Node\NonExistingFile;
+use OC\Files\Node\NonExistingFolder;
+use OC\Files\View;
use OC\FilesMetadata\Model\FilesMetadata;
use OCA\Files\Service\LivePhotosService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Exceptions\AbortedEventException;
use OCP\Files\Cache\CacheEntryRemovedEvent;
-use OCP\Files\Events\Node\AbstractNodesEvent;
use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
use OCP\Files\Events\Node\NodeCopiedEvent;
+use OCP\Files\File;
use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\FilesMetadata\IFilesMetadataManager;
@@ -37,6 +42,8 @@ class SyncLivePhotosListener implements IEventListener {
private ?Folder $userFolder,
private IFilesMetadataManager $filesMetadataManager,
private LivePhotosService $livePhotosService,
+ private IRootFolder $rootFolder,
+ private View $view,
) {
}
@@ -45,61 +52,47 @@ class SyncLivePhotosListener implements IEventListener {
return;
}
- $peerFileId = null;
-
- if ($event instanceof BeforeNodeRenamedEvent) {
- $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getSource()->getId());
- } elseif ($event instanceof BeforeNodeDeletedEvent) {
- $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getNode()->getId());
- } elseif ($event instanceof CacheEntryRemovedEvent) {
- $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getFileId());
- } elseif ($event instanceof BeforeNodeCopiedEvent || $event instanceof NodeCopiedEvent) {
- $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getSource()->getId());
- }
+ if ($event instanceof BeforeNodeCopiedEvent || $event instanceof NodeCopiedEvent) {
+ $this->handleCopyRecursive($event, $event->getSource(), $event->getTarget());
+ } else {
+ $peerFileId = null;
+
+ if ($event instanceof BeforeNodeRenamedEvent) {
+ $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getSource()->getId());
+ } elseif ($event instanceof BeforeNodeDeletedEvent) {
+ $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getNode()->getId());
+ } elseif ($event instanceof CacheEntryRemovedEvent) {
+ $peerFileId = $this->livePhotosService->getLivePhotoPeerId($event->getFileId());
+ }
- if ($peerFileId === null) {
- return; // Not a live photo.
- }
+ if ($peerFileId === null) {
+ return; // Not a live photo.
+ }
- // Check the user's folder.
- $peerFile = $this->userFolder->getFirstNodeById($peerFileId);
+ // Check the user's folder.
+ $peerFile = $this->userFolder->getFirstNodeById($peerFileId);
- if ($peerFile === null) {
- return; // Peer file not found.
- }
+ if ($peerFile === null) {
+ return; // Peer file not found.
+ }
- if ($event instanceof BeforeNodeRenamedEvent) {
- $this->handleMove($event, $peerFile, false);
- } elseif ($event instanceof BeforeNodeDeletedEvent) {
- $this->handleDeletion($event, $peerFile);
- } elseif ($event instanceof CacheEntryRemovedEvent) {
- $peerFile->delete();
- } elseif ($event instanceof BeforeNodeCopiedEvent) {
- $this->handleMove($event, $peerFile, true);
- } elseif ($event instanceof NodeCopiedEvent) {
- $this->handleCopy($event, $peerFile);
+ if ($event instanceof BeforeNodeRenamedEvent) {
+ $this->runMoveOrCopyChecks($event->getSource(), $event->getTarget(), $peerFile);
+ $this->handleMove($event->getSource(), $event->getTarget(), $peerFile);
+ } elseif ($event instanceof BeforeNodeDeletedEvent) {
+ $this->handleDeletion($event, $peerFile);
+ } elseif ($event instanceof CacheEntryRemovedEvent) {
+ $peerFile->delete();
+ }
}
}
- /**
- * 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(AbstractNodesEvent $event, Node $peerFile, bool $prepForCopyOnly = false): void {
- if (!($event instanceof BeforeNodeCopiedEvent) &&
- !($event instanceof BeforeNodeRenamedEvent)) {
- return;
- }
-
- $sourceFile = $event->getSource();
- $targetFile = $event->getTarget();
+ private function runMoveOrCopyChecks(Node $sourceFile, Node $targetFile, Node $peerFile): void {
$targetParent = $targetFile->getParent();
$sourceExtension = $sourceFile->getExtension();
$peerFileExtension = $peerFile->getExtension();
$targetName = $targetFile->getName();
+ $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension;
if (!str_ends_with($targetName, '.' . $sourceExtension)) {
throw new AbortedEventException('Cannot change the extension of a Live Photo');
@@ -111,15 +104,31 @@ class SyncLivePhotosListener implements IEventListener {
} catch (NotFoundException) {
}
- $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension;
- try {
- $targetParent->get($peerTargetName);
- throw new AbortedEventException('A file already exist at destination path of the Live Photo');
- } catch (NotFoundException) {
+ if (!($targetParent instanceof NonExistingFolder)) {
+ try {
+ $targetParent->get($peerTargetName);
+ throw new AbortedEventException('A file already exist at destination path of the Live Photo');
+ } catch (NotFoundException) {
+ }
}
+ }
+
+ /**
+ * 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(Node $sourceFile, Node $targetFile, Node $peerFile): void {
+ $targetParent = $targetFile->getParent();
+ $sourceExtension = $sourceFile->getExtension();
+ $peerFileExtension = $peerFile->getExtension();
+ $targetName = $targetFile->getName();
+ $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension;
// in case the rename was initiated from this listener, we stop right now
- if ($prepForCopyOnly || in_array($peerFile->getId(), $this->pendingRenames)) {
+ if (in_array($peerFile->getId(), $this->pendingRenames)) {
return;
}
@@ -130,39 +139,37 @@ class SyncLivePhotosListener implements IEventListener {
throw new AbortedEventException($ex->getMessage());
}
- array_diff($this->pendingRenames, [$sourceFile->getId()]);
+ $this->pendingRenames = array_diff($this->pendingRenames, [$sourceFile->getId()]);
}
/**
* handle copy, we already know if it is doable from BeforeNodeCopiedEvent, so we just copy the linked file
- *
- * @param NodeCopiedEvent $event
- * @param Node $peerFile
*/
- private function handleCopy(NodeCopiedEvent $event, Node $peerFile): void {
- $sourceFile = $event->getSource();
+ private function handleCopy(File $sourceFile, File $targetFile, File $peerFile): void {
$sourceExtension = $sourceFile->getExtension();
$peerFileExtension = $peerFile->getExtension();
- $targetFile = $event->getTarget();
$targetParent = $targetFile->getParent();
$targetName = $targetFile->getName();
$peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension;
- /**
- * let's use freshly set variable.
- * we copy the file and get its id. We already have the id of the current copy
- * We have everything to update metadata and keep the link between the 2 copies.
- */
- $newPeerFile = $peerFile->copy($targetParent->getPath() . '/' . $peerTargetName);
+
+ if ($targetParent->nodeExists($peerTargetName)) {
+ // If the copy was a folder copy, then the peer file already exists.
+ $targetPeerFile = $targetParent->get($peerTargetName);
+ } else {
+ // If the copy was a file copy, then we need to create the peer file.
+ $targetPeerFile = $peerFile->copy($targetParent->getPath() . '/' . $peerTargetName);
+ }
+
/** @var FilesMetadata $targetMetadata */
$targetMetadata = $this->filesMetadataManager->getMetadata($targetFile->getId(), true);
$targetMetadata->setStorageId($targetFile->getStorage()->getCache()->getNumericStorageId());
- $targetMetadata->setString('files-live-photo', (string)$newPeerFile->getId());
+ $targetMetadata->setString('files-live-photo', (string)$targetPeerFile->getId());
$this->filesMetadataManager->saveMetadata($targetMetadata);
/** @var FilesMetadata $peerMetadata */
- $peerMetadata = $this->filesMetadataManager->getMetadata($newPeerFile->getId(), true);
- $peerMetadata->setStorageId($newPeerFile->getStorage()->getCache()->getNumericStorageId());
+ $peerMetadata = $this->filesMetadataManager->getMetadata($targetPeerFile->getId(), true);
+ $peerMetadata->setStorageId($targetPeerFile->getStorage()->getCache()->getNumericStorageId());
$peerMetadata->setString('files-live-photo', (string)$targetFile->getId());
$this->filesMetadataManager->saveMetadata($peerMetadata);
}
@@ -193,4 +200,47 @@ class SyncLivePhotosListener implements IEventListener {
}
return;
}
+
+ /*
+ * Recursively get all the peer ids of a live photo.
+ * Needed when coping a folder.
+ *
+ * @param BeforeNodeCopiedEvent|NodeCopiedEvent $event
+ */
+ private function handleCopyRecursive(Event $event, Node $sourceNode, Node $targetNode): void {
+ if ($sourceNode instanceof Folder && $targetNode instanceof Folder) {
+ foreach ($sourceNode->getDirectoryListing() as $sourceChild) {
+ if ($event instanceof BeforeNodeCopiedEvent) {
+ if ($sourceChild instanceof Folder) {
+ $targetChild = new NonExistingFolder($this->rootFolder, $this->view, $targetNode->getPath() . '/' . $sourceChild->getName(), null, $targetNode);
+ } else {
+ $targetChild = new NonExistingFile($this->rootFolder, $this->view, $targetNode->getPath() . '/' . $sourceChild->getName(), null, $targetNode);
+ }
+ } elseif ($event instanceof NodeCopiedEvent) {
+ $targetChild = $targetNode->get($sourceChild->getName());
+ } else {
+ throw new Exception('Event is type is not supported');
+ }
+
+ $this->handleCopyRecursive($event, $sourceChild, $targetChild);
+ }
+ } elseif ($sourceNode instanceof File && $targetNode instanceof File) {
+ $peerFileId = $this->livePhotosService->getLivePhotoPeerId($sourceNode->getId());
+ if ($peerFileId === null) {
+ return;
+ }
+ $peerFile = $this->userFolder->getFirstNodeById($peerFileId);
+ if ($peerFile === null) {
+ return;
+ }
+
+ if ($event instanceof BeforeNodeCopiedEvent) {
+ $this->runMoveOrCopyChecks($sourceNode, $targetNode, $peerFile);
+ } elseif ($event instanceof NodeCopiedEvent) {
+ $this->handleCopy($sourceNode, $targetNode, $peerFile);
+ }
+ } else {
+ throw new Exception('Source and target type are not matching');
+ }
+ }
}
diff --git a/apps/files_versions/lib/Listener/VersionStorageMoveListener.php b/apps/files_versions/lib/Listener/VersionStorageMoveListener.php
index 0f7dad29fe2..d0a0bcf4a92 100644
--- a/apps/files_versions/lib/Listener/VersionStorageMoveListener.php
+++ b/apps/files_versions/lib/Listener/VersionStorageMoveListener.php
@@ -11,6 +11,7 @@ namespace OCA\Files_Versions\Listener;
use Exception;
use OC\Files\Node\NonExistingFile;
+use OC\Files\Node\NonExistingFolder;
use OCA\Files_Versions\Versions\IVersionBackend;
use OCA\Files_Versions\Versions\IVersionManager;
use OCA\Files_Versions\Versions\IVersionsImporterBackend;
@@ -130,7 +131,7 @@ class VersionStorageMoveListener implements IEventListener {
}
private function getNodeStorage(Node $node): IStorage {
- if ($node instanceof NonExistingFile) {
+ if ($node instanceof NonExistingFile || $node instanceof NonExistingFolder) {
return $node->getParent()->getStorage();
} else {
return $node->getStorage();
diff --git a/cypress/e2e/files/FilesUtils.ts b/cypress/e2e/files/FilesUtils.ts
index f435272b9a2..6a62ff77e86 100644
--- a/cypress/e2e/files/FilesUtils.ts
+++ b/cypress/e2e/files/FilesUtils.ts
@@ -159,7 +159,7 @@ export const createFolder = (folderName: string) => {
// TODO: replace by proper data-cy selectors
cy.get('[data-cy-upload-picker] .action-item__menutoggle').first().click()
- cy.contains('.upload-picker__menu-entry button', 'New folder').click()
+ cy.get('[data-cy-upload-picker-menu-entry="newFolder"] button').click()
cy.get('[data-cy-files-new-node-dialog]').should('be.visible')
cy.get('[data-cy-files-new-node-dialog-input]').type(`{selectall}${folderName}`)
cy.get('[data-cy-files-new-node-dialog-submit]').click()
diff --git a/cypress/e2e/files/LivePhotosUtils.ts b/cypress/e2e/files/LivePhotosUtils.ts
new file mode 100644
index 00000000000..6b0015affce
--- /dev/null
+++ b/cypress/e2e/files/LivePhotosUtils.ts
@@ -0,0 +1,107 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+import type { User } from '@nextcloud/cypress'
+
+type SetupInfo = {
+ snapshot: string
+ jpgFileId: number
+ movFileId: number
+ fileName: string
+ user: User
+}
+
+/**
+ *
+ * @param user
+ * @param fileName
+ * @param domain
+ * @param requesttoken
+ * @param metadata
+ */
+function setMetadata(user: User, fileName: string, requesttoken: string, metadata: object) {
+ cy.url().then(url => {
+ const hostname = new URL(url).hostname
+ cy.request({
+ method: 'PROPPATCH',
+ url: `http://${hostname}/remote.php/dav/files/${user.userId}/${fileName}`,
+ auth: { user: user.userId, pass: user.password },
+ headers: {
+ requesttoken,
+ },
+ body: `<?xml version="1.0"?>
+ <d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.org/ns">
+ <d:set>
+ <d:prop>
+ ${Object.entries(metadata).map(([key, value]) => `<${key}>${value}</${key}>`).join('\n')}
+ </d:prop>
+ </d:set>
+ </d:propertyupdate>`,
+ })
+ })
+
+}
+
+/**
+ *
+ * @param enable
+ */
+export function setShowHiddenFiles(enable: boolean) {
+ cy.get('[data-cy-files-navigation-settings-button]').click()
+ // Force:true because the checkbox is hidden by the pretty UI.
+ if (enable) {
+ cy.get('[data-cy-files-settings-setting="show_hidden"] input').check({ force: true })
+ } else {
+ cy.get('[data-cy-files-settings-setting="show_hidden"] input').uncheck({ force: true })
+ }
+ cy.get('[data-cy-files-navigation-settings]').type('{esc}')
+}
+
+/**
+ *
+ */
+export function setupLivePhotos(): Cypress.Chainable<SetupInfo> {
+ return cy.task('getVariable', { key: 'live-photos-data' })
+ .then((_setupInfo) => {
+ const setupInfo = _setupInfo as SetupInfo || {}
+ if (setupInfo.snapshot) {
+ cy.restoreState(setupInfo.snapshot)
+ } else {
+ let requesttoken: string
+
+ setupInfo.fileName = Math.random().toString(36).replace(/[^a-z]+/g, '').substring(0, 10)
+
+ cy.createRandomUser().then(_user => { setupInfo.user = _user })
+
+ cy.then(() => {
+ cy.uploadContent(setupInfo.user, new Blob(['jpg file'], { type: 'image/jpg' }), 'image/jpg', `/${setupInfo.fileName}.jpg`)
+ .then(response => { setupInfo.jpgFileId = parseInt(response.headers['oc-fileid']) })
+ cy.uploadContent(setupInfo.user, new Blob(['mov file'], { type: 'video/mov' }), 'video/mov', `/${setupInfo.fileName}.mov`)
+ .then(response => { setupInfo.movFileId = parseInt(response.headers['oc-fileid']) })
+
+ cy.login(setupInfo.user)
+ })
+
+ cy.visit('/apps/files')
+
+ cy.get('head').invoke('attr', 'data-requesttoken').then(_requesttoken => { requesttoken = _requesttoken as string })
+
+ cy.then(() => {
+ setMetadata(setupInfo.user, `${setupInfo.fileName}.jpg`, requesttoken, { 'nc:metadata-files-live-photo': setupInfo.movFileId })
+ setMetadata(setupInfo.user, `${setupInfo.fileName}.mov`, requesttoken, { 'nc:metadata-files-live-photo': setupInfo.jpgFileId })
+ })
+
+ cy.then(() => {
+ cy.saveState().then((value) => { setupInfo.snapshot = value })
+ cy.task('setVariable', { key: 'live-photos-data', value: setupInfo })
+ })
+ }
+ return cy.then(() => {
+ cy.login(setupInfo.user)
+ cy.visit('/apps/files')
+ return cy.wrap(setupInfo)
+ })
+ })
+}
diff --git a/cypress/e2e/files/live_photos.cy.ts b/cypress/e2e/files/live_photos.cy.ts
index 659cdc544ed..8eb4efaaec0 100644
--- a/cypress/e2e/files/live_photos.cy.ts
+++ b/cypress/e2e/files/live_photos.cy.ts
@@ -4,75 +4,34 @@
*/
import type { User } from '@nextcloud/cypress'
-import { clickOnBreadcrumbs, closeSidebar, copyFile, getRowForFile, getRowForFileId, renameFile, triggerActionForFile, triggerInlineActionForFileId } from './FilesUtils'
-
-/**
- *
- * @param user
- * @param fileName
- * @param domain
- * @param requesttoken
- * @param metadata
- */
-function setMetadata(user: User, fileName: string, domain: string, requesttoken: string, metadata: object) {
- cy.request({
- method: 'PROPPATCH',
- url: `http://${domain}/remote.php/dav/files/${user.userId}/${fileName}`,
- auth: { user: user.userId, pass: user.password },
- headers: {
- requesttoken,
- },
- body: `<?xml version="1.0"?>
- <d:propertyupdate xmlns:d="DAV:" xmlns:nc="http://nextcloud.org/ns">
- <d:set>
- <d:prop>
- ${Object.entries(metadata).map(([key, value]) => `<${key}>${value}</${key}>`).join('\n')}
- </d:prop>
- </d:set>
- </d:propertyupdate>`,
- })
-}
+import {
+ clickOnBreadcrumbs,
+ copyFile,
+ createFolder,
+ getRowForFile,
+ getRowForFileId,
+ moveFile,
+ navigateToFolder,
+ renameFile,
+ triggerActionForFile,
+ triggerInlineActionForFileId,
+} from './FilesUtils'
+import { setShowHiddenFiles, setupLivePhotos } from './LivePhotosUtils'
describe('Files: Live photos', { testIsolation: true }, () => {
- let currentUser: User
+ let user: User
let randomFileName: string
let jpgFileId: number
let movFileId: number
- let hostname: string
- let requesttoken: string
-
- before(() => {
- cy.createRandomUser().then((user) => {
- currentUser = user
- cy.login(currentUser)
- cy.visit('/apps/files')
- })
-
- cy.url().then(url => { hostname = new URL(url).hostname })
- })
beforeEach(() => {
- 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`)
- .then(response => { jpgFileId = parseInt(response.headers['oc-fileid']) })
- cy.uploadContent(currentUser, new Blob(['mov file'], { type: 'video/mov' }), 'video/mov', `/${randomFileName}.mov`)
- .then(response => { movFileId = parseInt(response.headers['oc-fileid']) })
-
- cy.login(currentUser)
- cy.visit('/apps/files')
-
- cy.get('head').invoke('attr', 'data-requesttoken').then(_requesttoken => { requesttoken = _requesttoken as string })
-
- cy.then(() => {
- setMetadata(currentUser, `${randomFileName}.jpg`, hostname, requesttoken, { 'nc:metadata-files-live-photo': movFileId })
- setMetadata(currentUser, `${randomFileName}.mov`, hostname, requesttoken, { 'nc:metadata-files-live-photo': jpgFileId })
- })
-
- cy.then(() => {
- cy.visit(`/apps/files/files/${jpgFileId}`) // Refresh and scroll to the .jpg file.
- closeSidebar()
- })
+ setupLivePhotos()
+ .then((setupInfo) => {
+ user = setupInfo.user
+ randomFileName = setupInfo.fileName
+ jpgFileId = setupInfo.jpgFileId
+ movFileId = setupInfo.movFileId
+ })
})
it('Only renders the .jpg file', () => {
@@ -81,12 +40,8 @@ describe('Files: Live photos', { testIsolation: true }, () => {
})
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 })
+ beforeEach(() => {
+ setShowHiddenFiles(true)
})
it("Shows both files when 'Show hidden files' is enabled", () => {
@@ -113,6 +68,35 @@ describe('Files: Live photos', { testIsolation: true }, () => {
getRowForFile(`${randomFileName} (copy).mov`).should('have.length', 1)
})
+ it('Keeps live photo link when copying folder', () => {
+ createFolder('folder')
+ moveFile(`${randomFileName}.jpg`, 'folder')
+ copyFile('folder', '.')
+ navigateToFolder('folder (copy)')
+
+ getRowForFile(`${randomFileName}.jpg`).should('have.length', 1)
+ getRowForFile(`${randomFileName}.mov`).should('have.length', 1)
+
+ setShowHiddenFiles(false)
+
+ getRowForFile(`${randomFileName}.jpg`).should('have.length', 1)
+ getRowForFile(`${randomFileName}.mov`).should('have.length', 0)
+ })
+
+ it('Block copying live photo in a folder containing a mov file with the same name', () => {
+ createFolder('folder')
+ cy.uploadContent(user, new Blob(['mov file'], { type: 'video/mov' }), 'video/mov', `/folder/${randomFileName}.mov`)
+ cy.login(user)
+ cy.visit('/apps/files')
+ copyFile(`${randomFileName}.jpg`, 'folder')
+ navigateToFolder('folder')
+
+ cy.get('[data-cy-files-list-row-fileid]').should('have.length', 1)
+ getRowForFile(`${randomFileName}.mov`).should('have.length', 1)
+ getRowForFile(`${randomFileName}.jpg`).should('have.length', 0)
+ getRowForFile(`${randomFileName} (copy).jpg`).should('have.length', 0)
+ })
+
it('Moves files when moving the .jpg', () => {
renameFile(`${randomFileName}.jpg`, `${randomFileName}_moved.jpg`)
clickOnBreadcrumbs('All files')
diff --git a/lib/private/Files/Node/HookConnector.php b/lib/private/Files/Node/HookConnector.php
index 423eea258ed..1149951174c 100644
--- a/lib/private/Files/Node/HookConnector.php
+++ b/lib/private/Files/Node/HookConnector.php
@@ -171,7 +171,7 @@ class HookConnector {
public function copy($arguments) {
$source = $this->getNodeForPath($arguments['oldpath']);
- $target = $this->getNodeForPath($arguments['newpath']);
+ $target = $this->getNodeForPath($arguments['newpath'], $source instanceof Folder);
$this->root->emit('\OC\Files', 'preCopy', [$source, $target]);
$this->dispatcher->dispatch('\OCP\Files::preCopy', new GenericEvent([$source, $target]));
@@ -203,7 +203,7 @@ class HookConnector {
$this->dispatcher->dispatchTyped($event);
}
- private function getNodeForPath(string $path): Node {
+ private function getNodeForPath(string $path, bool $isDir = false): Node {
$info = Filesystem::getView()->getFileInfo($path);
if (!$info) {
$fullPath = Filesystem::getView()->getAbsolutePath($path);
@@ -212,7 +212,7 @@ class HookConnector {
} else {
$info = null;
}
- if (Filesystem::is_dir($path)) {
+ if ($isDir || Filesystem::is_dir($path)) {
return new NonExistingFolder($this->root, $this->view, $fullPath, $info);
} else {
return new NonExistingFile($this->root, $this->view, $fullPath, $info);