Delete file shares through attachments API

Previously the file was deleted in the file structure of the user is not
expected as the file might not only be related to the card.

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl
2021-05-25 18:20:27 +02:00
parent e27e8d2ff6
commit 13dcacc3bb
4 changed files with 71 additions and 51 deletions

View File

@@ -959,6 +959,7 @@ For now only `deck_file` is supported as an attachment type.
### DELETE /boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId} - Delete an attachment ### DELETE /boards/{boardId}/stacks/{stackId}/cards/{cardId}/attachments/{attachmentId} - Delete an attachment
#### Request parameters #### Request parameters
| Parameter | Type | Description | | Parameter | Type | Description |

View File

@@ -35,6 +35,7 @@ use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\NoPermissionException; use OCA\Deck\NoPermissionException;
use OCA\Deck\NotFoundException; use OCA\Deck\NotFoundException;
use OCA\Deck\StatusException; use OCA\Deck\StatusException;
use OCP\AppFramework\Db\IMapperException;
use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\Response;
use OCP\ICache; use OCP\ICache;
use OCP\ICacheFactory; use OCP\ICacheFactory;
@@ -320,14 +321,10 @@ class AttachmentService {
* Either mark an attachment as deleted for later removal or just remove it depending * Either mark an attachment as deleted for later removal or just remove it depending
* on the IAttachmentService implementation * on the IAttachmentService implementation
* *
* @param $attachmentId * @throws NoPermissionException
* @return \OCP\AppFramework\Db\Entity * @throws NotFoundException
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws BadRequestException
*/ */
public function delete($cardId, $attachmentId, $type = 'deck_file') { public function delete(int $cardId, int $attachmentId, string $type = 'deck_file'): Attachment {
try { try {
$service = $this->getService($type); $service = $this->getService($type);
} catch (InvalidAttachmentType $e) { } catch (InvalidAttachmentType $e) {
@@ -340,40 +337,32 @@ class AttachmentService {
$attachment->setType($type); $attachment->setType($type);
$attachment->setCardId($cardId); $attachment->setCardId($cardId);
$service->extendData($attachment); $service->extendData($attachment);
$service->delete($attachment); } else {
$this->changeHelper->cardChanged($attachment->getCardId()); try {
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE); $attachment = $this->attachmentMapper->find($attachmentId);
return $attachment; } catch (IMapperException $e) {
throw new NoPermissionException('Permission denied');
}
} }
try {
$attachment = $this->attachmentMapper->find($attachmentId);
} catch (\Exception $e) {
throw new NoPermissionException('Permission denied');
}
$this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT); $this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
$this->cache->clear('card-' . $attachment->getCardId());
if ($service->allowUndo()) { if ($service->allowUndo()) {
$service->markAsDeleted($attachment); $service->markAsDeleted($attachment);
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE); $attachment = $this->attachmentMapper->update($attachment);
$this->changeHelper->cardChanged($attachment->getCardId()); } else {
return $this->attachmentMapper->update($attachment); $service->delete($attachment);
if (!$service instanceof ICustomAttachmentService) {
$attachment = $this->attachmentMapper->delete($attachment);
}
} }
$service->delete($attachment);
$attachment = $this->attachmentMapper->delete($attachment); $this->cache->clear('card-' . $attachment->getCardId());
$this->changeHelper->cardChanged($attachment->getCardId()); $this->changeHelper->cardChanged($attachment->getCardId());
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE); $this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
return $attachment; return $attachment;
} }
public function restore($cardId, $attachmentId, $type = 'deck_file') { public function restore(int $cardId, int $attachmentId, string $type = 'deck_file'): Attachment {
if (is_numeric($attachmentId) === false) {
throw new BadRequestException('attachment id must be a number');
}
try { try {
$attachment = $this->attachmentMapper->find($attachmentId); $attachment = $this->attachmentMapper->find($attachmentId);
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@@ -23,7 +23,10 @@
namespace OCA\Deck\Service; namespace OCA\Deck\Service;
use OCA\Deck\Db\Acl;
use OCA\Deck\Db\Attachment; use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\CardMapper;
use OCA\Deck\NoPermissionException;
use OCA\Deck\Sharing\DeckShareProvider; use OCA\Deck\Sharing\DeckShareProvider;
use OCA\Deck\StatusException; use OCA\Deck\StatusException;
use OCP\AppFramework\Http\StreamResponse; use OCP\AppFramework\Http\StreamResponse;
@@ -38,6 +41,7 @@ use OCP\IRequest;
use OCP\Share\Exceptions\ShareNotFound; use OCP\Share\Exceptions\ShareNotFound;
use OCP\Share\IManager; use OCP\Share\IManager;
use OCP\Share\IShare; use OCP\Share\IShare;
use Psr\Log\LoggerInterface;
class FilesAppService implements IAttachmentService, ICustomAttachmentService { class FilesAppService implements IAttachmentService, ICustomAttachmentService {
private $request; private $request;
@@ -48,8 +52,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
private $configService; private $configService;
private $l10n; private $l10n;
private $preview; private $preview;
private $permissionService;
private $mimeTypeDetector; private $mimeTypeDetector;
private $permissionService;
private $cardMapper;
private $logger;
public function __construct( public function __construct(
IRequest $request, IRequest $request,
@@ -59,8 +65,10 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
ConfigService $configService, ConfigService $configService,
DeckShareProvider $shareProvider, DeckShareProvider $shareProvider,
IPreview $preview, IPreview $preview,
PermissionService $permissionService,
IMimeTypeDetector $mimeTypeDetector, IMimeTypeDetector $mimeTypeDetector,
PermissionService $permissionService,
CardMapper $cardMapper,
LoggerInterface $logger,
string $userId = null string $userId = null
) { ) {
$this->request = $request; $this->request = $request;
@@ -72,15 +80,20 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$this->userId = $userId; $this->userId = $userId;
$this->preview = $preview; $this->preview = $preview;
$this->mimeTypeDetector = $mimeTypeDetector; $this->mimeTypeDetector = $mimeTypeDetector;
$this->permissionService = $permissionService;
$this->cardMapper = $cardMapper;
$this->logger = $logger;
} }
public function listAttachments(int $cardId): array { public function listAttachments(int $cardId): array {
$shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0); $shares = $this->shareProvider->getSharedWithByType($cardId, IShare::TYPE_DECK, -1, 0);
$shares = array_filter($shares, function ($share) { return array_filter(array_map(function (IShare $share) use ($cardId) {
return $share->getPermissions() > 0; try {
}); $file = $share->getNode();
return array_map(function (IShare $share) use ($cardId) { } catch (NotFoundException $e) {
$file = $share->getNode(); $this->logger->debug('Unable to find node for share with ID ' . $share->getId());
return null;
}
$attachment = new Attachment(); $attachment = new Attachment();
$attachment->setType('file'); $attachment->setType('file');
$attachment->setId((int)$share->getId()); $attachment->setId((int)$share->getId());
@@ -89,9 +102,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$attachment->setData($file->getName()); $attachment->setData($file->getName());
$attachment->setLastModified($file->getMTime()); $attachment->setLastModified($file->getMTime());
$attachment->setCreatedAt($share->getShareTime()->getTimestamp()); $attachment->setCreatedAt($share->getShareTime()->getTimestamp());
$attachment->setDeletedAt(0); $attachment->setDeletedAt($share->getPermissions() === 0 ? $share->getShareTime()->getTimestamp() : 0);
return $attachment; return $attachment;
}, $shares); }, $shares));
} }
public function getAttachmentCount(int $cardId): int { public function getAttachmentCount(int $cardId): int {
@@ -144,6 +157,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
} }
public function display(Attachment $attachment) { public function display(Attachment $attachment) {
// Problem: Folders
/** @psalm-suppress InvalidCatch */ /** @psalm-suppress InvalidCatch */
try { try {
$share = $this->shareProvider->getShareById($attachment->getId()); $share = $this->shareProvider->getShareById($attachment->getId());
@@ -165,6 +179,9 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$file = $this->getUploadedFile(); $file = $this->getUploadedFile();
$fileName = $file['name']; $fileName = $file['name'];
// get shares for current card
// check if similar filename already exists
$userFolder = $this->rootFolder->getUserFolder($this->userId); $userFolder = $this->rootFolder->getUserFolder($this->userId);
try { try {
$folder = $userFolder->get($this->configService->getAttachmentFolder()); $folder = $userFolder->get($this->configService->getAttachmentFolder());
@@ -245,12 +262,16 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
$file = $share->getNode(); $file = $share->getNode();
$attachment->setData($file->getName()); $attachment->setData($file->getName());
if ($file->getOwner() !== null && $file->getOwner()->getUID() === $this->userId) { // Deleting a Nextcloud file attachment will remove the share to the card, keeping the source file untouched
$file->delete(); // Opt-out of individual shares per user is no longer performed within deck but can still be done through the files app
$canEdit = $this->permissionService->checkPermission($this->cardMapper, $attachment->getCardId(), Acl::PERMISSION_EDIT);
$isFileOwner = $file->getOwner() !== null && $file->getOwner()->getUID() === $this->userId;
if ($isFileOwner || $canEdit) {
$this->shareManager->deleteShare($share);
return; return;
} }
$this->shareManager->deleteFromSelf($share, $this->userId); throw new NoPermissionException('No permission to remove the attachment from the card');
} }
public function allowUndo() { public function allowUndo() {

View File

@@ -22,7 +22,7 @@
<template> <template>
<AttachmentDragAndDrop :card-id="cardId" class="drop-upload--sidebar"> <AttachmentDragAndDrop :card-id="cardId" class="drop-upload--sidebar">
<div class="button-group"> <div class="button-group" v-if="!isReadOnly">
<button class="icon-upload" @click="uploadNewFile()"> <button class="icon-upload" @click="uploadNewFile()">
{{ t('deck', 'Upload new files') }} {{ t('deck', 'Upload new files') }}
</button> </button>
@@ -49,18 +49,25 @@
</li> </li>
<li v-for="attachment in attachments" <li v-for="attachment in attachments"
:key="attachment.id" :key="attachment.id"
class="attachment"> class="attachment"
:class="{ 'attachment--deleted': attachment.deletedAt > 0 }">
<a class="fileicon" <a class="fileicon"
:href="internalLink(attachment)"
:style="mimetypeForAttachment(attachment)" :style="mimetypeForAttachment(attachment)"
@click.prevent="showViewer(attachment)" /> @click.prevent="showViewer(attachment)" />
<div class="details"> <div class="details">
<a @click.prevent="showViewer(attachment)"> <a :href="internalLink(attachment)" @click.prevent="showViewer(attachment)">
<div class="filename"> <div class="filename">
<span class="basename">{{ attachment.data }}</span> <span class="basename">{{ attachment.data }}</span>
</div> </div>
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span> <div v-if="attachment.deletedAt === 0">
<span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span> <span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
<span class="filedate">{{ attachment.createdBy }}</span> <span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span>
<span class="filedate">{{ attachment.createdBy }}</span>
</div>
<div v-else>
<span class="attachment--info">{{ t('deck', 'Pending share') }}</span>
</div>
</a> </a>
</div> </div>
<Actions v-if="selectable"> <Actions v-if="selectable">
@@ -68,12 +75,12 @@
{{ t('deck', 'Add this attachment') }} {{ t('deck', 'Add this attachment') }}
</ActionButton> </ActionButton>
</Actions> </Actions>
<Actions v-if="removable" :force-menu="true"> <Actions v-if="removable && !isReadOnly" :force-menu="true">
<ActionLink v-if="attachment.extendedData.fileid" icon="icon-folder" :href="internalLink(attachment)"> <ActionLink v-if="attachment.extendedData.fileid" icon="icon-folder" :href="internalLink(attachment)">
{{ t('deck', 'Show in Files') }} {{ t('deck', 'Show in Files') }}
</ActionLink> </ActionLink>
<ActionButton v-if="attachment.extendedData.fileid" icon="icon-delete" @click="unshareAttachment(attachment)"> <ActionButton v-if="attachment.extendedData.fileid && !isReadOnly" icon="icon-delete" @click="unshareAttachment(attachment)">
{{ t('deck', 'Unshare file') }} {{ t('deck', 'Remove attachment') }}
</ActionButton> </ActionButton>
<ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('delete-attachment', attachment)"> <ActionButton v-if="!attachment.extendedData.fileid && attachment.deletedAt === 0" icon="icon-delete" @click="$emit('delete-attachment', attachment)">
@@ -143,6 +150,7 @@ export default {
}, },
computed: { computed: {
attachments() { attachments() {
// FIXME sort propertly by last modified / deleted at
return [...this.$store.getters.attachmentsByCard(this.cardId)].filter(attachment => attachment.deletedAt >= 0).sort((a, b) => b.id - a.id) return [...this.$store.getters.attachmentsByCard(this.cardId)].filter(attachment => attachment.deletedAt >= 0).sort((a, b) => b.id - a.id)
}, },
mimetypeForAttachment() { mimetypeForAttachment() {
@@ -320,9 +328,10 @@ export default {
opacity: 0.7; opacity: 0.7;
} }
} }
.attachment--info,
.filesize, .filedate { .filesize, .filedate {
font-size: 90%; font-size: 90%;
color: darkgray; color: var(--color-text-maxcontrast);
} }
.app-popover-menu-utils { .app-popover-menu-utils {
position: relative; position: relative;