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:
@@ -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 |
|
||||||
|
|||||||
@@ -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());
|
|
||||||
$this->activityManager->triggerEvent(ActivityManager::DECK_OBJECT_CARD, $attachment, ActivityManager::SUBJECT_ATTACHMENT_DELETE);
|
|
||||||
return $attachment;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$attachment = $this->attachmentMapper->find($attachmentId);
|
$attachment = $this->attachmentMapper->find($attachmentId);
|
||||||
} catch (\Exception $e) {
|
} catch (IMapperException $e) {
|
||||||
throw new NoPermissionException('Permission denied');
|
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);
|
$service->delete($attachment);
|
||||||
|
if (!$service instanceof ICustomAttachmentService) {
|
||||||
$attachment = $this->attachmentMapper->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) {
|
||||||
|
|||||||
@@ -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 {
|
||||||
});
|
|
||||||
return array_map(function (IShare $share) use ($cardId) {
|
|
||||||
$file = $share->getNode();
|
$file = $share->getNode();
|
||||||
|
} catch (NotFoundException $e) {
|
||||||
|
$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() {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<div v-if="attachment.deletedAt === 0">
|
||||||
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
|
<span class="filesize">{{ formattedFileSize(attachment.extendedData.filesize) }}</span>
|
||||||
<span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span>
|
<span class="filedate">{{ relativeDate(attachment.createdAt*1000) }}</span>
|
||||||
<span class="filedate">{{ attachment.createdBy }}</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;
|
||||||
|
|||||||
Reference in New Issue
Block a user