From 5c293f605916832bb7fc774f1f376dfd66aef0e5 Mon Sep 17 00:00:00 2001 From: Jonas Date: Thu, 15 Oct 2026 07:25:40 +0200 Subject: [PATCH] fix(attachments): rewrite attachment file ids when copying folders When a folder with Markdown files is copied, the attachment folders are renamed and the relative `.attachments./` links are updated, but `/f/` links (e.g. embedded whiteboards) kept pointing at the attachments of the source file. Users without access to the source then saw broken attachments in the copy. Map source attachments to their copies by file name and rewrite the file ids, as is already done when copying a single file. Signed-off-by: Jonas Assisted-by: ClaudeCode:claude-opus-5-5 --- lib/Service/AttachmentService.php | 11 +++- tests/unit/Service/AttachmentServiceTest.php | 66 ++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/lib/Service/AttachmentService.php b/lib/Service/AttachmentService.php index 2373dfb0a47..8ecc9f6a612 100755 --- a/lib/Service/AttachmentService.php +++ b/lib/Service/AttachmentService.php @@ -719,9 +719,18 @@ public function copyAttachmentsInFolder(Folder $sourceFolder, Folder $targetFold continue; } - if ($targetNode instanceof File && $targetAttachmentDir instanceof Folder) { + if ($targetNode instanceof File + && $sourceAttachmentDir instanceof Folder + && $targetAttachmentDir instanceof Folder) { + $fileIdMapping = []; + foreach ($sourceAttachmentDir->getDirectoryListing() as $sourceAttachment) { + if ($sourceAttachment instanceof File && $targetAttachmentDir->nodeExists($sourceAttachment->getName())) { + $fileIdMapping[] = [$sourceAttachment->getId(), $targetAttachmentDir->get($sourceAttachment->getName())->getId()]; + } + } $targetAttachmentDir->move($targetFolder->getPath() . '/' . $targetAttachmentDirName); self::replaceAttachmentFolderId($sourceNode, $targetNode); + self::replaceAttachmentFileIds($targetNode, $fileIdMapping); } } } diff --git a/tests/unit/Service/AttachmentServiceTest.php b/tests/unit/Service/AttachmentServiceTest.php index f915d658c31..2d9d70aca66 100755 --- a/tests/unit/Service/AttachmentServiceTest.php +++ b/tests/unit/Service/AttachmentServiceTest.php @@ -6,6 +6,14 @@ use OCA\Text\Service\AttachmentService; use OCP\Files\File; use OCP\Files\Folder; +use OCP\Files\IFilenameValidator; +use OCP\Files\IMimeTypeDetector; +use OCP\Files\IRootFolder; +use OCP\FilesMetadata\IFilesMetadataManager; +use OCP\IPreview; +use OCP\ISession; +use OCP\IURLGenerator; +use OCP\Share\IManager as ShareManager; use PHPUnit\Framework\TestCase; class AttachmentServiceTest extends TestCase { @@ -267,4 +275,62 @@ public function testReplaceAttachmentFileIdNoReplace(string $sourceContent): voi AttachmentService::replaceAttachmentFileIds($target, [[1, 2]]); $this->assertEquals($sourceContent, $replacedContent); } + + public function testCopyAttachmentsInFolderRewritesFolderAndFileIds(): void { + $service = new AttachmentService( + $this->createMock(IRootFolder::class), + $this->createMock(ShareManager::class), + $this->createMock(IPreview::class), + $this->createMock(IMimeTypeDetector::class), + $this->createMock(IURLGenerator::class), + $this->createMock(IFilenameValidator::class), + $this->createMock(IFilesMetadataManager::class), + $this->createMock(ISession::class), + ); + + $sourceMd = $this->createMock(File::class); + $sourceMd->method('getId')->willReturn(1); + $sourceMd->method('getName')->willReturn('page.md'); + $sourceMd->method('getMimeType')->willReturn('text/markdown'); + + $sourceAttachment = $this->createMock(File::class); + $sourceAttachment->method('getId')->willReturn(10); + $sourceAttachment->method('getName')->willReturn('board.whiteboard'); + + $sourceAttachmentDir = $this->createMock(Folder::class); + $sourceAttachmentDir->method('getDirectoryListing')->willReturn([$sourceAttachment]); + + $sourceFolder = $this->createMock(Folder::class); + $sourceFolder->method('getDirectoryListing')->willReturn([$sourceMd]); + $sourceFolder->method('get')->with('.attachments.1')->willReturn($sourceAttachmentDir); + + $content = "![a.png](.attachments.1/a.png)\n\n[board](https://localhost/f/10 (preview))\n"; + $targetMd = $this->createMock(File::class); + $targetMd->method('getId')->willReturn(2); + $targetMd->method('getContent')->willReturnCallback(function () use (&$content) { + return $content; + }); + $targetMd->method('putContent')->willReturnCallback(function (string $new) use (&$content): void { + $content = $new; + }); + + $targetAttachment = $this->createMock(File::class); + $targetAttachment->method('getId')->willReturn(20); + + $targetAttachmentDir = $this->createMock(Folder::class); + $targetAttachmentDir->method('nodeExists')->with('board.whiteboard')->willReturn(true); + $targetAttachmentDir->method('get')->with('board.whiteboard')->willReturn($targetAttachment); + $targetAttachmentDir->expects($this->once())->method('move')->with('/target/.attachments.2'); + + $targetFolder = $this->createMock(Folder::class); + $targetFolder->method('getPath')->willReturn('/target'); + $targetFolder->method('get')->willReturnMap([ + ['page.md', $targetMd], + ['.attachments.1', $targetAttachmentDir], + ]); + + $service->copyAttachmentsInFolder($sourceFolder, $targetFolder); + + $this->assertEquals("![a.png](.attachments.2/a.png)\n\n[board](https://localhost/f/20 (preview))\n", $content); + } }