diff --git a/appinfo/database.xml b/appinfo/database.xml index febac5f47..9e70ea0b5 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -223,7 +223,7 @@ last_modified integer - + 8 false true @@ -231,11 +231,17 @@ created_at integer - + 8 false true + + created_by + text + true + 64 + diff --git a/appinfo/routes.php b/appinfo/routes.php index 6d0838d2b..b139ca638 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -59,6 +59,12 @@ return [ ['name' => 'card#assignUser', 'url' => '/cards/{cardId}/assign', 'verb' => 'POST'], ['name' => 'card#unassignUser', 'url' => '/cards/{cardId}/assign/{userId}', 'verb' => 'DELETE'], + ['name' => 'attachment#list', 'url' => '/cards/{cardId}/attachments', 'verb' => 'GET'], + ['name' => 'attachment#display', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'GET'], + ['name' => 'attachment#create', 'url' => '/cards/{cardId}/attachment', 'verb' => 'POST'], + ['name' => 'attachment#update', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'UPDATE'], + ['name' => 'attachment#delete', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'DELETE'], + // labels ['name' => 'label#create', 'url' => '/labels', 'verb' => 'POST'], ['name' => 'label#update', 'url' => '/labels/{labelId}', 'verb' => 'PUT'], diff --git a/lib/Controller/AttachmentController.php b/lib/Controller/AttachmentController.php new file mode 100644 index 000000000..096c4a3bc --- /dev/null +++ b/lib/Controller/AttachmentController.php @@ -0,0 +1,71 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Deck\Controller; + + +use OCA\Deck\Service\AttachmentService; +use OCP\AppFramework\Controller; +use OCP\IRequest; + +class AttachmentController extends Controller { + + /** @var AttachmentService */ + private $attachmentService; + + public function __construct($appName, IRequest $request, AttachmentService $attachmentService) { + parent::__construct($appName, $request); + $this->attachmentService = $attachmentService; + } + + public function list($cardId) { + return $this->attachmentService->getAll($cardId); + } + + /** + * @param $cardId + * @param $attachmentId + * @NoCSRFRequired + * @return \OCP\AppFramework\Http\Response + * @throws \OCA\Deck\NotFoundException + */ + public function display($cardId, $attachmentId) { + return $this->attachmentService->display($cardId, $attachmentId); + } + + public function create($cardId) { + return $this->attachmentService->create( + $cardId, + $this->request->getParam('type'), + $this->request->getParam('data') + ); + } + + public function update($cardId, $attachmentId) { + return $this->attachmentService->update($cardId, $attachmentId, $this->request->getParam('data')); + } + + public function delete($cardId, $attachmentId) { + return $this->attachmentService->delete($cardId, $attachmentId); + } +} \ No newline at end of file diff --git a/lib/Controller/BoardController.php b/lib/Controller/BoardController.php index a0ed9790a..0ecd044b1 100644 --- a/lib/Controller/BoardController.php +++ b/lib/Controller/BoardController.php @@ -26,12 +26,13 @@ namespace OCA\Deck\Controller; use OCA\Deck\Db\Acl; use OCA\Deck\Service\BoardService; use OCA\Deck\Service\PermissionService; +use OCP\AppFramework\ApiController; use OCP\IRequest; use OCP\AppFramework\Controller; use OCP\IUserManager; use OCP\IGroupManager; -class BoardController extends Controller { +class BoardController extends ApiController { private $userId; private $boardService; private $userManager; diff --git a/lib/Db/Attachment.php b/lib/Db/Attachment.php new file mode 100644 index 000000000..cc1ce7abc --- /dev/null +++ b/lib/Db/Attachment.php @@ -0,0 +1,47 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Deck\Db; + +class Attachment extends RelationalEntity { + + protected $cardId; + protected $type; + protected $data; + protected $lastModified; + protected $createdAt; + protected $createdBy; + protected $deletedAt; + protected $extendedData = []; + + public function __construct() { + $this->addType('id', 'integer'); + $this->addType('cardId', 'integer'); + $this->addType('lastModified', 'integer'); + $this->addType('createdAt', 'integer'); + $this->addType('deletedAt', 'integer'); + $this->addResolvable('createdBy'); + $this->addRelation('extendedData'); + } + +} \ No newline at end of file diff --git a/lib/Db/AttachmentMapper.php b/lib/Db/AttachmentMapper.php new file mode 100644 index 000000000..a67174f24 --- /dev/null +++ b/lib/Db/AttachmentMapper.php @@ -0,0 +1,126 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + + +namespace OCA\Deck\Db; + +use OCP\AppFramework\Db\Entity; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IUserManager; +use PDO; + + +class AttachmentMapper extends DeckMapper implements IPermissionMapper { + + private $cardMapper; + private $userManager; + + public function __construct(IDBConnection $db, CardMapper $cardMapper, IUserManager $userManager) { + parent::__construct($db, 'deck_attachment', Attachment::class); + $this->cardMapper = $cardMapper; + $this->userManager = $userManager; + $this->qb = $this->db->getQueryBuilder(); + } + + /** + * @param $id + * @return Entity|Attachment + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + */ + public function find($id) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('deck_attachment') + ->where($qb->expr()->eq('id', $id)); + + $cursor = $qb->execute(); + $row = $cursor->fetch(PDO::FETCH_ASSOC); + $cursor->closeCursor(); + return $this->mapRowToEntity($row); + } + + /** + * Find all attachments for a card + * + * @param $cardId + * @return array + */ + public function findAll($cardId) { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('deck_attachment') + ->where($qb->expr()->eq('card_id', $cardId, IQueryBuilder::PARAM_INT)); + + $entities = []; + $cursor = $qb->execute(); + while($row = $cursor->fetch()){ + $entities[] = $this->mapRowToEntity($row); + } + $cursor->closeCursor(); + return $entities; + } + + /** + * @param Attachment $attachment + * @throws \Exception + */ + public function mapParticipant(Attachment $attachment) { + $userManager = $this->userManager; + $attachment->resolveRelation('participant', function() use (&$userManager, &$attachment) { + $user = $userManager->get($attachment->getParticipant()); + if ($user !== null) { + return new User($user); + } + return null; + }); + } + + + /** + * Check if $userId is owner of Entity with $id + * + * @param $userId string userId + * @param $id int|string unique entity identifier + * @return boolean + */ + public function isOwner($userId, $id) { + // TODO: Implement isOwner() method. + } + + /** + * Query boardId for Entity of given $id + * + * @param $id int|string unique entity identifier + * @return int|null id of Board + */ + public function findBoardId($id) { + try { + $attachment = $this->find($id); + } catch (\Exception $e) { + return null; + } + $this->cardMapper->findBoardId($attachment->getCardId()); + } +} \ No newline at end of file diff --git a/lib/Db/Card.php b/lib/Db/Card.php index d2c3aee28..9b50fdf06 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -35,6 +35,7 @@ class Card extends RelationalEntity { protected $createdAt; protected $labels; protected $assignedUsers; + protected $attachments; protected $owner; protected $order; protected $archived = false; @@ -58,6 +59,7 @@ class Card extends RelationalEntity { $this->addType('notified', 'boolean'); $this->addRelation('labels'); $this->addRelation('assignedUsers'); + $this->addRelation('attachments'); $this->addRelation('participants'); $this->addResolvable('owner'); } diff --git a/lib/Db/DeckMapper.php b/lib/Db/DeckMapper.php index 98295d37d..2e6c19cc6 100644 --- a/lib/Db/DeckMapper.php +++ b/lib/Db/DeckMapper.php @@ -25,6 +25,13 @@ namespace OCA\Deck\Db; use OCP\AppFramework\Db\Mapper; +/** + * Class DeckMapper + * + * @package OCA\Deck\Db + * + * TODO: Move to QBMapper once Nextcloud 14 is a minimum requirement + */ abstract class DeckMapper extends Mapper { /** diff --git a/lib/InvalidAttachmentType.php b/lib/InvalidAttachmentType.php new file mode 100644 index 000000000..1b43b680b --- /dev/null +++ b/lib/InvalidAttachmentType.php @@ -0,0 +1,37 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Deck; + + +class InvalidAttachmentType extends \Exception { + + /** + * InvalidAttachmentType constructor. + */ + public function __construct($type) { + + parent::__construct('No matching IAttachmentService implementation found for type ' . $type); + + } +} \ No newline at end of file diff --git a/lib/Service/AttachmentService.php b/lib/Service/AttachmentService.php new file mode 100644 index 000000000..5d50fb5d8 --- /dev/null +++ b/lib/Service/AttachmentService.php @@ -0,0 +1,197 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Deck\Service; + + +use OCA\Deck\AppInfo\Application; +use OCA\Deck\Db\Acl; +use OCA\Deck\Db\Attachment; +use OCA\Deck\Db\AttachmentMapper; +use OCA\Deck\Db\CardMapper; +use OCA\Deck\InvalidAttachmentType; +use OCA\Deck\NotFoundException; +use OCP\AppFramework\Http\Response; + +class AttachmentService { + + private $attachmentMapper; + private $cardMapper; + private $permissionService; + private $userId; + + /** @var IAttachmentService[] */ + private $services = []; + private $application; + + /** + * AttachmentService constructor. + * + * @param AttachmentMapper $attachmentMapper + * @param CardMapper $cardMapper + * @param PermissionService $permissionService + * @param $userId + * @throws \OCP\AppFramework\QueryException + */ + public function __construct(AttachmentMapper $attachmentMapper, CardMapper $cardMapper, PermissionService $permissionService, Application $application, $userId) { + $this->attachmentMapper = $attachmentMapper; + $this->cardMapper = $cardMapper; + $this->permissionService = $permissionService; + $this->userId = $userId; + $this->application = $application; + + // Register shipped attachment services + // TODO: move this to a plugin based approach once we have different types of attachments + $this->registerAttachmentService('deck_file', FileService::class); + } + + /** + * @param string $type + * @param string $class + * @throws \OCP\AppFramework\QueryException + */ + public function registerAttachmentService($type, $class) { + $this->services[$type] = $this->application->getContainer()->query($class); + } + + /** + * @param string $type + * @return IAttachmentService + * @throws InvalidAttachmentType + */ + public function getService($type) { + if (isset($this->services[$type])) { + return $this->services[$type]; + } + throw new InvalidAttachmentType($type); + } + + /** + * @param $cardId + * @return array + * @throws \OCA\Deck\NoPermissionException + */ + public function findAll($cardId) { + $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ); + + $attachments = $this->attachmentMapper->findAll($cardId); + foreach ($attachments as &$attachment) { + try { + $service = $this->getService($attachment->getType()); + $service->extendData($attachment); + } catch (InvalidAttachmentType $e) { + // Ingore invalid attachment types when extending the data + } + } + return $attachments; + } + + public function create($cardId, $type, $data) { + $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); + + $attachment = new Attachment(); + $attachment->setCardId($cardId); + $attachment->setType($type); + $attachment->setData($data); + $attachment->setCreatedBy($this->userId); + $attachment->setLastModified(time()); + $attachment->setCreatedAt(time()); + + try { + $service = $this->getService($attachment->getType()); + $service->create($attachment); + } catch (InvalidAttachmentType $e) { + // just store the data + } + $attachment = $this->attachmentMapper->insert($attachment); + + // extend data so the frontend can use it properly after creating + try { + $service = $this->getService($attachment->getType()); + $service->extendData($attachment); + } catch (InvalidAttachmentType $e) { + // just store the data + } + return $attachment; + } + + + /** + * Display the attachment + * + * @param $cardId + * @param $attachmentId + * @return Response + * @throws NotFoundException + */ + public function display($cardId, $attachmentId) { + $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ); + + $attachment = $this->attachmentMapper->find($attachmentId); + + try { + $service = $this->getService($attachment->getType()); + return $service->display($attachment); + } catch (InvalidAttachmentType $e) { + throw new NotFoundException(); + } + } + + /** + * Update an attachment with custom data + * + * @param $cardId + * @param $attachmentId + * @param $request + * @return mixed + * @throws \OCA\Deck\NoPermissionException + * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException + */ + public function update($cardId, $attachmentId, $data) { + $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); + $attachment = $this->attachmentMapper->find($attachmentId); + $attachment->setData($data); + try { + $service = $this->getService($attachment->getType()); + $service->update($attachment); + } catch (InvalidAttachmentType $e) { + // just update without further action + } + return $attachment; + } + + public function delete($cardId, $attachmentId) { + $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); + + $attachment = $this->attachmentMapper->find($attachmentId); + try { + $service = $this->getService($attachment->getType()); + $service->delete($attachment); + } catch (InvalidAttachmentType $e) { + // just delete without further action + } + $this->attachmentMapper->delete($attachment); + return $attachment; + } +} \ No newline at end of file diff --git a/lib/Service/CardService.php b/lib/Service/CardService.php index 5376f2474..c9adcebe5 100644 --- a/lib/Service/CardService.php +++ b/lib/Service/CardService.php @@ -41,19 +41,22 @@ class CardService { private $boardService; private $assignedUsersMapper; - public function __construct(CardMapper $cardMapper, StackMapper $stackMapper, PermissionService $permissionService, BoardService $boardService, AssignedUsersMapper $assignedUsersMapper) { + public function __construct(CardMapper $cardMapper, StackMapper $stackMapper, PermissionService $permissionService, BoardService $boardService, AssignedUsersMapper $assignedUsersMapper, AttachmentService $attachmentService) { $this->cardMapper = $cardMapper; $this->stackMapper = $stackMapper; $this->permissionService = $permissionService; $this->boardService = $boardService; $this->assignedUsersMapper = $assignedUsersMapper; + $this->attachmentService = $attachmentService; } public function find($cardId) { $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ); $card = $this->cardMapper->find($cardId); $assignedUsers = $this->assignedUsersMapper->find($card->getId()); + $attachments = $this->attachmentService->findAll($cardId); $card->setAssignedUsers($assignedUsers); + $card->setAttachments($attachments); return $card; } diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php new file mode 100644 index 000000000..417027e21 --- /dev/null +++ b/lib/Service/FileService.php @@ -0,0 +1,152 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Deck\Service; + + +use OCA\Deck\Db\Attachment; +use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\Files\SimpleFS\ISimpleFile; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\IL10N; +use OCP\IRequest; + + +class FileService implements IAttachmentService { + + private $l10n; + private $appData; + private $request; + + public function __construct( + IL10N $l10n, + IAppData $appData, + IRequest $request + ) { + $this->l10n = $l10n; + $this->appData = $appData; + $this->request = $request; + } + + /** + * @param Attachment $attachment + * @return ISimpleFile + * @throws NotFoundException + * @throws NotPermittedException + */ + private function getFileForAttachment(Attachment $attachment) { + return $this->getFolder($attachment) + ->getFile($attachment->getData()); + } + + /** + * @param Attachment $attachment + * @return ISimpleFolder + * @throws NotPermittedException + */ + private function getFolder(Attachment $attachment) { + $folderName = 'file-card-' . (int)$attachment->getCardId(); + try { + $folder = $this->appData->getFolder($folderName); + } catch (NotFoundException $e) { + $folder = $this->appData->newFolder($folderName); + } + return $folder; + } + + public function extendData(Attachment $attachment) { + try { + $file = $this->getFileForAttachment($attachment); + } catch (NotFoundException $e) { + // TODO: log error + return $attachment; + } catch (NotPermittedException $e) { + return $attachment; + } + $attachment->setExtendedData([ + 'filesize' => $file->getSize(), + 'mimetype' => $file->getMimeType(), + 'info' => pathinfo($file->getName()) + ]); + return $attachment; + } + + public function create(Attachment $attachment) { + $file = $this->request->getUploadedFile('file'); + $cardId = $attachment->getCardId(); + $error = null; + $phpFileUploadErrors = [ + UPLOAD_ERR_OK => $this->l10n->t('The file was uploaded'), + UPLOAD_ERR_INI_SIZE => $this->l10n->t('The uploaded file exceeds the upload_max_filesize directive in php.ini'), + UPLOAD_ERR_FORM_SIZE => $this->l10n->t('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'), + UPLOAD_ERR_PARTIAL => $this->l10n->t('The file was only partially uploaded'), + UPLOAD_ERR_NO_FILE => $this->l10n->t('No file was uploaded'), + UPLOAD_ERR_NO_TMP_DIR => $this->l10n->t('Missing a temporary folder'), + UPLOAD_ERR_CANT_WRITE => $this->l10n->t('Could not write file to disk'), + UPLOAD_ERR_EXTENSION => $this->l10n->t('A PHP extension stopped the file upload'), + ]; + + if (empty($file)) { + $error = $this->l10n->t('No file uploaded'); + } + if (!empty($file) && array_key_exists('error', $file) && $file['error'] !== UPLOAD_ERR_OK) { + $error = $phpFileUploadErrors[$file['error']]; + } + if ($error !== null) { + throw new \RuntimeException($error); + } + + $folder = $this->getFolder($attachment); + $fileName = $file['name']; + if ($folder->fileExists($fileName)) { + throw new \Exception('File already exists.'); + } + $target = $folder->newFile($fileName); + $target->putContent(file_get_contents($file['tmp_name'], 'r')); + + $attachment->setData($fileName); + } + + public function update(Attachment $attachment) { + $file = $this->getFileForAttachment($attachment); + + } + + public function delete(Attachment $attachment) { + try { + $file = $this->getFileForAttachment($attachment); + $file->delete(); + } catch (NotFoundException $e) { + } + } + + public function display(Attachment $attachment) { + $file = $this->getFileForAttachment($attachment); + $response = new FileDisplayResponse($file); + $response->addHeader('Content-Type', $file->getMimeType()); + return $response; + } +} \ No newline at end of file diff --git a/lib/Service/IAttachmentService.php b/lib/Service/IAttachmentService.php new file mode 100644 index 000000000..1210637a6 --- /dev/null +++ b/lib/Service/IAttachmentService.php @@ -0,0 +1,84 @@ + + * + * @author Julius Härtl + * + * @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 . + * + */ + +namespace OCA\Deck\Service; + + +use OCA\Deck\Db\Attachment; +use OCP\AppFramework\Http\Response; + +/** + * Interface IAttachmentService + * + * Implement this interface to extend the default attachment behaviour + * This interface allows to extend/reduce the data stored with an attachment, + * as well as rendering a custom output per attachment type + * + */ +interface IAttachmentService { + + /** + * Add extended data to the returned data of an attachment + * + * @param Attachment $attachment + * @return mixed + */ + public function extendData(Attachment $attachment); + + /** + * Display the attachment + * + * TODO: Move to IAttachmentDisplayService for better separation + * + * @param Attachment $attachment + * @return Response + */ + public function display(Attachment $attachment); + + /** + * Create a new attachment + * + * This method will be called before inserting the attachment entry in the database + * + * @param Attachment $attachment + */ + public function create(Attachment $attachment); + + /** + * Update an attachment with custom data + * + * This method will be called before updating the attachment entry in the database + * + * @param Attachment $attachment + */ + public function update(Attachment $attachment); + + /** + * Delete an attachment + * + * This method will be called before removing the attachment entry from the database + * + * @param Attachment $attachment + */ + public function delete(Attachment $attachment); +} \ No newline at end of file