diff options
-rw-r--r-- | apps/dav/lib/Connector/Sabre/Directory.php | 8 | ||||
-rw-r--r-- | apps/files/lib/Listener/SyncLivePhotosListener.php | 184 | ||||
-rw-r--r-- | apps/files_versions/lib/Listener/VersionStorageMoveListener.php | 3 | ||||
-rw-r--r-- | cypress/e2e/files/FilesUtils.ts | 2 | ||||
-rw-r--r-- | cypress/e2e/files/LivePhotosUtils.ts | 107 | ||||
-rw-r--r-- | cypress/e2e/files/live_photos.cy.ts | 120 | ||||
-rw-r--r-- | lib/private/Files/Node/HookConnector.php | 6 |
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); |