Allow to undo file deletions

Signed-off-by: Julius Härtl <jus@bitgrid.net>
This commit is contained in:
Julius Härtl
2018-06-12 15:33:06 +02:00
parent 0c711b2b0b
commit ee5a54a575
12 changed files with 152 additions and 20 deletions

View File

@@ -64,6 +64,8 @@ return [
['name' => 'attachment#create', 'url' => '/cards/{cardId}/attachment', 'verb' => 'POST'], ['name' => 'attachment#create', 'url' => '/cards/{cardId}/attachment', 'verb' => 'POST'],
['name' => 'attachment#update', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'UPDATE'], ['name' => 'attachment#update', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'UPDATE'],
['name' => 'attachment#delete', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'DELETE'], ['name' => 'attachment#delete', 'url' => '/cards/{cardId}/attachment/{attachmentId}', 'verb' => 'DELETE'],
['name' => 'attachment#restore', 'url' => '/cards/{cardId}/attachment/{attachmentId}/restore', 'verb' => 'GET'],
// labels // labels
['name' => 'label#create', 'url' => '/labels', 'verb' => 'POST'], ['name' => 'label#create', 'url' => '/labels', 'verb' => 'POST'],

View File

@@ -780,6 +780,10 @@ input.input-inline {
li.attachment { li.attachment {
display: flex; display: flex;
&.deleted {
opacity: .5;
}
.fileicon { .fileicon {
display: inline-block; display: inline-block;
min-width: 32px; min-width: 32px;

View File

@@ -50,11 +50,9 @@ app.controller('CardController', function ($scope, $rootScope, $sce, $location,
$scope.uploader.uploadItem(fileItem); $scope.uploader.uploadItem(fileItem);
}; };
$scope.uploader.onAfterAddingFile = function(fileItem) { $scope.uploader.onAfterAddingFile = function(fileItem) {
console.log(fileItem);
let existingFile = $scope.cardservice.getCurrent().attachments.find((attachment) => { let existingFile = $scope.cardservice.getCurrent().attachments.find((attachment) => {
return attachment.data === fileItem.file.name; return attachment.data === fileItem.file.name;
}); });
console.log(existingFile);
if (typeof existingFile !== 'undefined') { if (typeof existingFile !== 'undefined') {
OC.dialogs.confirm( OC.dialogs.confirm(
`A file with the name ${fileItem.file.name} already exists. Do you want to overwrite it?`, `A file with the name ${fileItem.file.name} already exists. Do you want to overwrite it?`,

View File

@@ -137,9 +137,18 @@ app.factory('CardService', function (ApiService, $http, $q) {
var deferred = $q.defer(); var deferred = $q.defer();
var self = this; var self = this;
$http.delete(this.baseUrl + '/' + this.getCurrent().id + '/attachment/' + attachment.id, {}).then(function (response) { $http.delete(this.baseUrl + '/' + this.getCurrent().id + '/attachment/' + attachment.id, {}).then(function (response) {
self.getCurrent().attachments = self.getCurrent().attachments.filter(function (obj) { if (response.data.de#letedAt > 0) {
return obj.id !== attachment.id; let currentAttachment = self.getCurrent().attachments.find(function (obj) {
}); if (obj.id === attachment.id) {
obj.deletedAt = response.data.deletedAt;
}
});
} else {
self.getCurrent().attachments = self.getCurrent().attachments.filter(function (obj) {
return obj.id !== attachment.id;
});
}
deferred.resolve(response.data); deferred.resolve(response.data);
}, function (error) { }, function (error) {
deferred.reject('Error when removing the attachment'); deferred.reject('Error when removing the attachment');
@@ -147,6 +156,22 @@ app.factory('CardService', function (ApiService, $http, $q) {
return deferred.promise; return deferred.promise;
}; };
CardService.prototype.attachmentRemoveUndo = function (attachment) {
var deferred = $q.defer();
var self = this;
$http.get(this.baseUrl + '/' + this.getCurrent().id + '/attachment/' + attachment.id + '/restore', {}).then(function (response) {
let currentAttachment = self.getCurrent().attachments.find(function (obj) {
if (obj.id === attachment.id) {
obj.deletedAt = response.data.deletedAt;
}
});
deferred.resolve(response.data);
}, function (error) {
deferred.reject('Error when restoring the attachment');
});
return deferred.promise;
};
var service = new CardService($http, 'cards', $q); var service = new CardService($http, 'cards', $q);
return service; return service;
}); });

View File

@@ -68,4 +68,8 @@ class AttachmentController extends Controller {
public function delete($cardId, $attachmentId) { public function delete($cardId, $attachmentId) {
return $this->attachmentService->delete($cardId, $attachmentId); return $this->attachmentService->delete($cardId, $attachmentId);
} }
public function restore($cardId, $attachmentId) {
return $this->attachmentService->restore($cardId, $attachmentId);
}
} }

View File

@@ -21,25 +21,28 @@
* *
*/ */
/**
* Created by PhpStorm.
* User: jus
* Date: 16.05.17
* Time: 12:34
*/
namespace OCA\Deck\Cron; namespace OCA\Deck\Cron;
use OC\BackgroundJob\Job; use OC\BackgroundJob\Job;
use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\BoardMapper; use OCA\Deck\Db\BoardMapper;
use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\Service\AttachmentService;
class DeleteCron extends Job { class DeleteCron extends Job {
/** @var BoardMapper */ /** @var BoardMapper */
private $boardMapper; private $boardMapper;
/** @var AttachmentService */
private $attachmentService;
/** @var AttachmentMapper */
private $attachmentMapper;
public function __construct(BoardMapper $boardMapper) { public function __construct(BoardMapper $boardMapper, AttachmentService $attachmentService, AttachmentMapper $attachmentMapper) {
$this->boardMapper = $boardMapper; $this->boardMapper = $boardMapper;
$this->attachmentService = $attachmentService;
$this->attachmentMapper = $attachmentMapper;
} }
/** /**
@@ -51,6 +54,18 @@ class DeleteCron extends Job {
foreach ($boards as $board) { foreach ($boards as $board) {
$this->boardMapper->delete($board); $this->boardMapper->delete($board);
} }
$attachments = $this->attachmentMapper->findToDelete();
foreach ($attachments as $attachment) {
try {
$service = $this->attachmentService->getService($attachment->getType());
$service->delete($attachment);
} catch (InvalidAttachmentType $e) {
// Just delete the attachment if no service is available
}
$this->attachmentMapper->delete($attachment);
}
} }
} }

View File

@@ -28,10 +28,10 @@ class Attachment extends RelationalEntity {
protected $cardId; protected $cardId;
protected $type; protected $type;
protected $data; protected $data;
protected $lastModified; protected $lastModified = 0;
protected $createdAt; protected $createdAt = 0;
protected $createdBy; protected $createdBy;
protected $deletedAt; protected $deletedAt = 0;
protected $extendedData = []; protected $extendedData = [];
public function __construct() { public function __construct() {

View File

@@ -82,6 +82,24 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
return $entities; return $entities;
} }
public function findToDelete() {
// add buffer of 5 min
$timeLimit = time() - (60 * 5);
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('deck_attachment')
->where($qb->expr()->gt('deleted_at', '0', IQueryBuilder::PARAM_INT))
->andWhere($qb->expr()->lt('deleted_at', (string)$timeLimit, IQueryBuilder::PARAM_INT));
$entities = [];
$cursor = $qb->execute();
while($row = $cursor->fetch()){
$entities[] = $this->mapRowToEntity($row);
}
$cursor->closeCursor();
return $entities;
}
/** /**
* @param Attachment $attachment * @param Attachment $attachment
* @throws \Exception * @throws \Exception

View File

@@ -30,6 +30,7 @@ use OCA\Deck\Db\Attachment;
use OCA\Deck\Db\AttachmentMapper; use OCA\Deck\Db\AttachmentMapper;
use OCA\Deck\Db\CardMapper; use OCA\Deck\Db\CardMapper;
use OCA\Deck\InvalidAttachmentType; use OCA\Deck\InvalidAttachmentType;
use OCA\Deck\NoPermissionException;
use OCA\Deck\NotFoundException; use OCA\Deck\NotFoundException;
use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\Response;
@@ -181,17 +182,46 @@ class AttachmentService {
return $attachment; return $attachment;
} }
/**
* Either mark an attachment as deleted for later removal or just remove it depending
* on the IAttachmentService implementation
*
* @param $cardId
* @param $attachmentId
* @return \OCP\AppFramework\Db\Entity
* @throws \OCA\Deck\NoPermissionException
* @throws \OCP\AppFramework\Db\DoesNotExistException
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
*/
public function delete($cardId, $attachmentId) { public function delete($cardId, $attachmentId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT); $this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
$attachment = $this->attachmentMapper->find($attachmentId); $attachment = $this->attachmentMapper->find($attachmentId);
try { try {
$service = $this->getService($attachment->getType()); $service = $this->getService($attachment->getType());
if ($service->allowUndo()) {
$service->markAsDeleted($attachment);
return $this->attachmentMapper->update($attachment);
}
$service->delete($attachment); $service->delete($attachment);
} catch (InvalidAttachmentType $e) { } catch (InvalidAttachmentType $e) {
// just delete without further action // just delete without further action
} }
$this->attachmentMapper->delete($attachment); return $this->attachmentMapper->delete($attachment);
return $attachment; }
public function restore($cardId, $attachmentId) {
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_EDIT);
$attachment = $this->attachmentMapper->find($attachmentId);
try {
$service = $this->getService($attachment->getType());
if ($service->allowUndo()) {
$attachment->setDeletedAt(0);
return $this->attachmentMapper->update($attachment);
}
} catch (InvalidAttachmentType $e) {
}
throw new NoPermissionException();
} }
} }

View File

@@ -149,4 +149,22 @@ class FileService implements IAttachmentService {
$response->addHeader('Content-Type', $file->getMimeType()); $response->addHeader('Content-Type', $file->getMimeType());
return $response; return $response;
} }
/**
* Should undo be allowed and the delete action be done by a background job
*
* @return bool
*/
public function allowUndo() {
return true;
}
/**
* Mark an attachment as deleted
*
* @param Attachment $attachment
*/
public function markAsDeleted(Attachment $attachment) {
$attachment->setDeletedAt(time());
}
} }

View File

@@ -81,4 +81,19 @@ interface IAttachmentService {
* @param Attachment $attachment * @param Attachment $attachment
*/ */
public function delete(Attachment $attachment); public function delete(Attachment $attachment);
/**
* Should undo be allowed and the delete action be done by a background job
*
* @return bool
*/
public function allowUndo();
/**
* Mark an attachment as deleted
*
* @param Attachment $attachment
*/
public function markAsDeleted(Attachment $attachment);
} }

View File

@@ -94,7 +94,7 @@
</div> </div>
<div class="section-content card-attachments" v-if="cardservice.getCurrent().attachments"> <div class="section-content card-attachments" v-if="cardservice.getCurrent().attachments">
<ul> <ul>
<li class="attachment" ng-repeat="attachment in cardservice.getCurrent().attachments | orderBy: '-lastModified'"> <li class="attachment" ng-repeat="attachment in cardservice.getCurrent().attachments | orderBy: ['deletedAt', '-lastModified']" ng-class="{deleted: attachment.deletedAt > 0}">
<a class="fileicon" ng-style="mimetypeForAttachment(attachment)" ng-href="{{ attachmentUrl(attachment) }}"></a> <a class="fileicon" ng-style="mimetypeForAttachment(attachment)" ng-href="{{ attachmentUrl(attachment) }}"></a>
<div class="details"> <div class="details">
<a ng-href="{{ attachmentUrl(attachment) }}" target="_blank"> <a ng-href="{{ attachmentUrl(attachment) }}" target="_blank">
@@ -106,12 +106,15 @@
<span class="filedate">{{ attachment.createdAt|relativeDateFilter }}</span> <span class="filedate">{{ attachment.createdAt|relativeDateFilter }}</span>
</a> </a>
</div> </div>
<div class="app-popover-menu-utils"> <button class="icon icon-history button-inline" ng-click="cardservice.attachmentRemoveUndo(attachment)" ng-if="attachment.deletedAt > 0" title="<?php p($l->t('Undo file deletion - Otherwise the file will be deleted during the next cronjob run.')); ?>">
<span class="hidden-visually"><?php p($l->t('Undo file deletion')); ?></span>
</button>
<div class="app-popover-menu-utils" ng-if="attachment.deletedAt == 0">
<button class="button-inline icon icon-more" ng-model="attachment"></button> <button class="button-inline icon icon-more" ng-model="attachment"></button>
<div class="popovermenu hidden"> <div class="popovermenu hidden">
<ul> <ul>
<li> <li>
<a class="menuitem action action-delete permanent" <a class="menuitem action action-delete"
ng-click="cardservice.attachmentRemove(attachment); $event.stopPropagation();"><span ng-click="cardservice.attachmentRemove(attachment); $event.stopPropagation();"><span
class="icon icon-delete"></span><span><?php p($l->t('Delete')); ?></span></a> class="icon icon-delete"></span><span><?php p($l->t('Delete')); ?></span></a>
</li> </li>