diff options
author | Julien Veyssier <eneiluj@posteo.net> | 2021-11-25 18:38:13 +0300 |
---|---|---|
committer | Julien Veyssier <eneiluj@posteo.net> | 2022-01-03 12:27:37 +0300 |
commit | 551729e08d7c044ae34d8972129caa448725f7a9 (patch) | |
tree | fa810c00048ae6de3a92ffa965aac2cd57aba51c /lib | |
parent | fc56680d9621a7fb8cc9fd75d0ad0230de3c9884 (diff) |
implement auto attachment management for file deletion/move/copy
Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/AppInfo/Application.php | 10 | ||||
-rw-r--r-- | lib/Listeners/BeforeNodeDeletedListener.php | 50 | ||||
-rw-r--r-- | lib/Listeners/BeforeNodeRenamedListener.php | 53 | ||||
-rw-r--r-- | lib/Listeners/NodeCopiedListener.php | 55 | ||||
-rw-r--r-- | lib/Service/ImageService.php | 116 |
5 files changed, 273 insertions, 11 deletions
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d15b783fa..a370e8056 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -27,6 +27,9 @@ namespace OCA\Text\AppInfo; use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent; +use OCA\Text\Listeners\BeforeNodeDeletedListener; +use OCA\Text\Listeners\BeforeNodeRenamedListener; +use OCA\Text\Listeners\NodeCopiedListener; use OCA\Text\Listeners\FilesLoadAdditionalScriptsListener; use OCA\Text\Listeners\FilesSharingLoadAdditionalScriptsListener; use OCA\Text\Listeners\LoadViewerListener; @@ -37,6 +40,9 @@ use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\DirectEditing\RegisterDirectEditorEvent; +use OCP\Files\Events\Node\BeforeNodeDeletedEvent; +use OCP\Files\Events\Node\BeforeNodeRenamedEvent; +use OCP\Files\Events\Node\NodeCopiedEvent; use OCP\Files\Template\ITemplateManager; use OCP\Files\Template\TemplateFileCreator; use OCP\IL10N; @@ -53,6 +59,10 @@ class Application extends App implements IBootstrap { $context->registerEventListener(LoadViewer::class, LoadViewerListener::class); $context->registerEventListener(LoadAdditionalScriptsEvent::class, FilesLoadAdditionalScriptsListener::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, FilesSharingLoadAdditionalScriptsListener::class); + // for image attachments + $context->registerEventListener(NodeCopiedEvent::class, NodeCopiedListener::class); + $context->registerEventListener(BeforeNodeRenamedEvent::class, BeforeNodeRenamedListener::class); + $context->registerEventListener(BeforeNodeDeletedEvent::class, BeforeNodeDeletedListener::class); } public function boot(IBootContext $context): void { diff --git a/lib/Listeners/BeforeNodeDeletedListener.php b/lib/Listeners/BeforeNodeDeletedListener.php new file mode 100644 index 000000000..eac7b28d6 --- /dev/null +++ b/lib/Listeners/BeforeNodeDeletedListener.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Julien Veyssier <eneiluj@posteo.net> + * + * @author Julien Veyssier <eneiluj@posteo.net> + * + * @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\Text\Listeners; + +use OCA\Text\Service\ImageService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\Node\BeforeNodeDeletedEvent; +use OCP\Files\File; + +class BeforeNodeDeletedListener implements IEventListener { + + private ImageService $imageService; + + public function __construct(ImageService $imageService) { + $this->imageService = $imageService; + } + + public function handle(Event $event): void { + if (!$event instanceof BeforeNodeDeletedEvent) { + return; + } + if ($event->getNode() instanceof File && $event->getNode()->getMimeType() === 'text/markdown') { + $this->imageService->deleteAttachments($event->getNode()); + } + } +} diff --git a/lib/Listeners/BeforeNodeRenamedListener.php b/lib/Listeners/BeforeNodeRenamedListener.php new file mode 100644 index 000000000..483c2f9a6 --- /dev/null +++ b/lib/Listeners/BeforeNodeRenamedListener.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Julien Veyssier <eneiluj@posteo.net> + * + * @author Julien Veyssier <eneiluj@posteo.net> + * + * @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\Text\Listeners; + +use OCA\Text\Service\ImageService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\Node\BeforeNodeRenamedEvent; +use OCP\Files\File; + +class BeforeNodeRenamedListener implements IEventListener { + + private ImageService $imageService; + + public function __construct(ImageService $imageService) { + $this->imageService = $imageService; + } + + public function handle(Event $event): void { + if (!$event instanceof BeforeNodeRenamedEvent) { + return; + } + if ($event->getSource() instanceof File + && $event->getSource()->getMimeType() === 'text/markdown' + && $event->getTarget() instanceof File + ) { + $this->imageService->moveAttachments($event->getSource(), $event->getTarget()); + } + } +} diff --git a/lib/Listeners/NodeCopiedListener.php b/lib/Listeners/NodeCopiedListener.php new file mode 100644 index 000000000..64b66c166 --- /dev/null +++ b/lib/Listeners/NodeCopiedListener.php @@ -0,0 +1,55 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Julien Veyssier <eneiluj@posteo.net> + * + * @author Julien Veyssier <eneiluj@posteo.net> + * + * @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\Text\Listeners; + +use OCA\Text\Service\ImageService; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\Node\NodeCopiedEvent; +use OCP\Files\File; + +class NodeCopiedListener implements IEventListener { + + private ImageService $imageService; + + public function __construct(ImageService $imageService) { + $this->imageService = $imageService; + } + + public function handle(Event $event): void { + if (!$event instanceof NodeCopiedEvent) { + return; + } + error_log('COPIED source ' . $event->getSource()->getId() . ' target '. $event->getTarget()->getId()); + if ($event->getSource() instanceof File + && $event->getSource()->getMimeType() === 'text/markdown' + && $event->getTarget() instanceof File + && $event->getTarget()->getMimeType() === 'text/markdown' + ) { + $this->imageService->copyAttachments($event->getSource(), $event->getTarget()); + } + } +} diff --git a/lib/Service/ImageService.php b/lib/Service/ImageService.php index ae68a08a0..e9defe1a8 100644 --- a/lib/Service/ImageService.php +++ b/lib/Service/ImageService.php @@ -432,6 +432,25 @@ class ImageService { return null; } + private function getAttachmentDirectoryForFile(File $textFile): ?Folder { + $owner = $textFile->getOwner(); + $ownerId = $owner->getUID(); + $ownerUserFolder = $this->rootFolder->getUserFolder($ownerId); + $ownerTextFile = $ownerUserFolder->getById($textFile->getId()); + if (count($ownerTextFile) > 0) { + $ownerTextFile = $ownerTextFile[0]; + $ownerParentFolder = $ownerTextFile->getParent(); + $attachmentFolderName = '.attachments.' . $textFile->getId(); + if ($ownerParentFolder->nodeExists($attachmentFolderName)) { + $attachmentFolder = $ownerParentFolder->get($attachmentFolderName); + if ($attachmentFolder instanceof Folder) { + return $attachmentFolder; + } + } + } + return null; + } + /** * Get a user file from file ID * @param string $filePath @@ -574,17 +593,7 @@ class ImageService { $attachmentsById[$attNode->getId()] = $attNode; } - // get IDs of attachment listed in the markdown file content - $matches = []; - preg_match_all( - '/\!\[[^\[\]]+\]\(text:\/\/image\?[^)]*imageFileId=([\d]+)[^)]*\)/', - $textFile->getContent(), - $matches, - PREG_SET_ORDER - ); - $contentAttachmentIds = array_map(function (array $match) { - return $match[1] ?? null; - }, $matches); + $contentAttachmentIds = $this->getAttachmentIdsFromContent($textFile->getContent()); $toDelete = array_diff(array_keys($attachmentsById), $contentAttachmentIds); foreach ($toDelete as $id) { @@ -595,4 +604,89 @@ class ImageService { } return 0; } + + + /** + * Get IDs of attachment listed in the markdown file content + * + * @param string $content + * @return array + */ + private function getAttachmentIdsFromContent(string $content): array { + $matches = []; + preg_match_all( + '/\!\[[^\[\]]+\]\(text:\/\/image\?[^)]*imageFileId=([\d]+)[^)]*\)/', + $content, + $matches, + PREG_SET_ORDER + ); + return array_map(function (array $match) { + return $match[1] ?? null; + }, $matches); + } + + /** + * @param File $source + * @param File $target + * @throws NotFoundException + * @throws \OCP\Files\InvalidPathException + * @throws \OCP\Files\NotPermittedException + * @throws \OCP\Lock\LockedException + */ + public function moveAttachments(File $source, File $target): void { + // if the parent directory has changed + if ($source->getParent()->getPath() !== $target->getParent()->getPath()) { + // if there is an attachment dir for this file + $sourceAttachmentDir = $this->getAttachmentDirectoryForFile($source); + if ($sourceAttachmentDir !== null) { + $sourceAttachmentDir->move($target->getParent()->getPath() . '/' . $sourceAttachmentDir->getName()); + } + } + } + + /** + * @param File $source + * @throws NotFoundException + * @throws \OCP\Files\InvalidPathException + * @throws \OCP\Files\NotPermittedException + */ + public function deleteAttachments(File $source): void { + // if there is an attachment dir for this file + $sourceAttachmentDir = $this->getAttachmentDirectoryForFile($source); + if ($sourceAttachmentDir !== null) { + $sourceAttachmentDir->delete(); + } + } + + public function copyAttachments(File $source, File $target): void { + $sourceAttachmentDir = $this->getAttachmentDirectoryForFile($source); + if ($sourceAttachmentDir !== null) { + // create a new attachment dir next to the new file + $targetAttachmentDir = $this->getOrCreateAttachmentDirectoryForFile($target); + $markdownContent = $source->getContent(); + $sourceAttachmentIds = $this->getAttachmentIdsFromContent($markdownContent); + // replace the text file ID in each attachment link in the markdown content + $markdownContent = preg_replace( + '/\!\[([^\[\]]+)\]\(text:\/\/image\?textFileId=' . $source->getId() . '&imageFileId=([\d]+[^)]*)\)/', + '![$1](text://image?textFileId=' . $target->getId() . '&imageFileId=$2)', + $markdownContent + ); + // copy the attachments and replace attachment file IDs in the markdown content + foreach ($sourceAttachmentIds as $sourceAttachmentId) { + $sourceAttachment = $sourceAttachmentDir->getById($sourceAttachmentId); + if (count($sourceAttachment) > 0 && $sourceAttachment[0] instanceof File) { + $sourceAttachment = $sourceAttachment[0]; + $copied = $targetAttachmentDir->newFile($sourceAttachment->getName(), $sourceAttachment->getContent()); + $copiedId = $copied->getId(); + $markdownContent = preg_replace( + '/\!\[([^\[\]]+)\]\(text:\/\/image\?([^)]*)imageFileId=' . $sourceAttachmentId . '([^)]*)\)/', + '![$1](text://image?$2imageFileId=' . $copiedId . '$3)', + $markdownContent + ); + } + } + // FIXME this is not possible because the target file is locked at this point + $target->putContent($markdownContent); + } + } } |