Delete single attachments

This commit is contained in:
Eike Kettner 2020-04-26 23:04:03 +02:00
parent 916a9dbcc9
commit a939839041
11 changed files with 149 additions and 25 deletions

View File

@ -4,6 +4,7 @@
*Unknown*
- Allow to delete attachments of an item.
- Allow to be notified via e-mail for items with a due date. This uses
the periodic-task framework introduced in the last release.
- Fix issues when converting HTML with unkown links. This especially

View File

@ -66,7 +66,7 @@ trait OItem[F[_]] {
def getProposals(item: Ident, collective: Ident): F[MetaProposalList]
def delete(itemId: Ident, collective: Ident): F[Int]
def deleteItem(itemId: Ident, collective: Ident): F[Int]
def findAttachmentMeta(id: Ident, collective: Ident): F[Option[RAttachmentMeta]]
@ -74,6 +74,7 @@ trait OItem[F[_]] {
def findByFileSource(checksum: String, sourceId: Ident): F[Vector[RItem]]
def deleteAttachment(id: Ident, collective: Ident): F[Int]
}
object OItem {
@ -292,7 +293,7 @@ object OItem {
.attempt
.map(AddResult.fromUpdate)
def delete(itemId: Ident, collective: Ident): F[Int] =
def deleteItem(itemId: Ident, collective: Ident): F[Int] =
QItem.delete(store)(itemId, collective)
def getProposals(item: Ident, collective: Ident): F[MetaProposalList] =
@ -310,5 +311,7 @@ object OItem {
items <- OptionT.liftF(QItem.findByChecksum(checksum, coll))
} yield items).getOrElse(Vector.empty))
def deleteAttachment(id: Ident, collective: Ident): F[Int] =
QAttachment.deleteSingleAttachment(store)(id, collective)
})
}

View File

@ -1168,6 +1168,28 @@ paths:
$ref: "#/components/schemas/ItemProposals"
/sec/attachment/{id}:
delete:
tags: [ Attachment ]
summary: Delete an attachment.
description: |
Deletes a single attachment with all its related data like
file, the original file, extracted text, results from analysis
etc.
If the attachment is part of an archive, the archive is only
deleted, if it is the last entry left. Archives are otherwise
not deleted, if there are remaining attachments available.
security:
- authTokenHeader: []
parameters:
- $ref: "#/components/parameters/id"
responses:
200:
description: Ok
content:
application/json:
schema:
$ref: "#/components/schemas/BasicResult"
head:
tags: [ Attachment ]
summary: Get an attachment file.

View File

@ -119,6 +119,14 @@ object AttachmentRoutes {
md = rm.map(Conversions.mkAttachmentMeta)
resp <- md.map(Ok(_)).getOrElse(NotFound(BasicResult(false, "Not found.")))
} yield resp
case DELETE -> Root / Ident(id) =>
for {
n <- backend.item.deleteAttachment(id, user.account.collective)
res = if (n == 0) BasicResult(false, "Attachment not found")
else BasicResult(true, "Attachment deleted.")
resp <- Ok(res)
} yield resp
}
}

View File

@ -138,7 +138,7 @@ object ItemRoutes {
case DELETE -> Root / Ident(id) =>
for {
n <- backend.item.delete(id, user.account.collective)
n <- backend.item.deleteItem(id, user.account.collective)
res = BasicResult(n > 0, if (n > 0) "Item deleted" else "Item deletion failed.")
resp <- Ok(res)
} yield resp

View File

@ -30,6 +30,9 @@ case class Column(name: String, ns: String = "", alias: String = "") {
def is(c: Column): Fragment =
f ++ fr"=" ++ c.f
def isSubquery(sq: Fragment): Fragment =
f ++ fr"=" ++ fr"(" ++ sq ++ fr")"
def isNot[A: Put](value: A): Fragment =
f ++ fr"<> $value"

View File

@ -15,28 +15,39 @@ import docspell.common.syntax.all._
object QAttachment {
private[this] val logger = org.log4s.getLogger
def deleteById[F[_]: Sync](store: Store[F])(attachId: Ident, coll: Ident): F[Int] =
/** Deletes an attachment, its related source and meta data records.
* It will only delete an related archive file, if this is the last
* attachment in that archive.
*/
def deleteSingleAttachment[F[_]: Sync](
store: Store[F]
)(attachId: Ident, coll: Ident): F[Int] = {
val loadFiles = for {
ra <- RAttachment.findByIdAndCollective(attachId, coll).map(_.map(_.fileId))
rs <- RAttachmentSource.findByIdAndCollective(attachId, coll).map(_.map(_.fileId))
ne <- RAttachmentArchive.countEntries(attachId)
} yield (ra, rs, ne)
for {
raFile <- store
.transact(RAttachment.findByIdAndCollective(attachId, coll))
.map(_.map(_.fileId))
rsFile <- store
.transact(RAttachmentSource.findByIdAndCollective(attachId, coll))
.map(_.map(_.fileId))
aaFile <- store
.transact(RAttachmentArchive.findByIdAndCollective(attachId, coll))
.map(_.map(_.fileId))
files <- store.transact(loadFiles)
k <- if (files._3 == 1) deleteArchive(store)(attachId)
else store.transact(RAttachmentArchive.delete(attachId))
n <- store.transact(RAttachment.delete(attachId))
f <- Stream
.emits(raFile.toSeq ++ rsFile.toSeq ++ aaFile.toSeq)
.emits(files._1.toSeq ++ files._2.toSeq)
.map(_.id)
.flatMap(store.bitpeace.delete)
.map(flag => if (flag) 1 else 0)
.compile
.foldMonoid
} yield n + f
} yield n + k + f
}
def deleteAttachment[F[_]: Sync](store: Store[F])(ra: RAttachment): F[Int] =
/** This deletes the attachment and *all* its related files. This used
* when deleting an item and should not be used to delete a
* *single* attachment where the item should stay.
*/
private def deleteAttachment[F[_]: Sync](store: Store[F])(ra: RAttachment): F[Int] =
for {
_ <- logger.fdebug[F](s"Deleting attachment: ${ra.id.id}")
s <- store.transact(RAttachmentSource.findById(ra.id))

View File

@ -124,6 +124,8 @@ object RAttachment {
q.query[(RAttachment, FileMeta)].to[Vector]
}
/** Deletes the attachment and its related source and meta records.
*/
def delete(attachId: Ident): ConnectionIO[Int] =
for {
n0 <- RAttachmentMeta.delete(attachId)

View File

@ -91,4 +91,13 @@ object RAttachmentArchive {
.to[Vector]
}
/** If the given attachment id has an associated archive, this returns
* the number of all associated attachments. Returns 0 if there is
* no archive for the given attachment.
*/
def countEntries(attachId: Ident): ConnectionIO[Int] = {
val qFileId = selectSimple(Seq(fileId), table, id.is(attachId))
val q = selectCount(id, table, fileId.isSubquery(qFileId))
q.query[Int].unique
}
}

View File

@ -3,6 +3,7 @@ module Api exposing
, changePassword
, checkCalEvent
, createMailSettings
, deleteAttachment
, deleteEquip
, deleteItem
, deleteMailSettings
@ -183,6 +184,23 @@ checkCalEvent flags input receive =
--- Delete Attachment
deleteAttachment :
Flags
-> String
-> (Result Http.Error BasicResult -> msg)
-> Cmd msg
deleteAttachment flags attachId receive =
Http2.authDelete
{ url = flags.config.baseUrl ++ "/api/v1/sec/attachment/" ++ attachId
, account = getAccount flags
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
--- Attachment Metadata

View File

@ -60,7 +60,7 @@ type alias Model =
, nameModel : String
, notesModel : Maybe String
, notesField : NotesField
, deleteConfirm : Comp.YesNoDimmer.Model
, deleteItemConfirm : Comp.YesNoDimmer.Model
, itemDatePicker : DatePicker
, itemDate : Maybe Int
, itemProposals : ItemProposals
@ -75,6 +75,7 @@ type alias Model =
, attachMeta : Dict String Comp.AttachmentMeta.Model
, attachMetaOpen : Bool
, pdfNativeView : Bool
, deleteAttachConfirm : Comp.YesNoDimmer.Model
}
@ -147,7 +148,7 @@ emptyModel =
, nameModel = ""
, notesModel = Nothing
, notesField = ViewNotes
, deleteConfirm = Comp.YesNoDimmer.emptyModel
, deleteItemConfirm = Comp.YesNoDimmer.emptyModel
, itemDatePicker = Comp.DatePicker.emptyModel
, itemDate = Nothing
, itemProposals = Api.Model.ItemProposals.empty
@ -162,6 +163,7 @@ emptyModel =
, attachMeta = Dict.empty
, attachMetaOpen = False
, pdfNativeView = False
, deleteAttachConfirm = Comp.YesNoDimmer.emptyModel
}
@ -198,7 +200,7 @@ type Msg
| SetDueDateSuggestion Int
| ItemDatePickerMsg Comp.DatePicker.Msg
| DueDatePickerMsg Comp.DatePicker.Msg
| YesNoMsg Comp.YesNoDimmer.Msg
| DeleteItemConfirm Comp.YesNoDimmer.Msg
| RequestDelete
| SaveResp (Result Http.Error BasicResult)
| DeleteResp (Result Http.Error BasicResult)
@ -215,6 +217,9 @@ type Msg
| AttachMetaClick String
| AttachMetaMsg String Comp.AttachmentMeta.Msg
| TogglePdfNativeView
| RequestDeleteAttachment String
| DeleteAttachConfirm String Comp.YesNoDimmer.Msg
| DeleteAttachResp (Result Http.Error BasicResult)
@ -676,10 +681,10 @@ update key flags next msg model =
RemoveDueDate ->
( { model | dueDate = Nothing }, setDueDate flags model Nothing )
YesNoMsg m ->
DeleteItemConfirm m ->
let
( cm, confirmed ) =
Comp.YesNoDimmer.update m model.deleteConfirm
Comp.YesNoDimmer.update m model.deleteItemConfirm
cmd =
if confirmed then
@ -688,10 +693,10 @@ update key flags next msg model =
else
Cmd.none
in
( { model | deleteConfirm = cm }, cmd )
( { model | deleteItemConfirm = cm }, cmd )
RequestDelete ->
update key flags next (YesNoMsg Comp.YesNoDimmer.activate) model
update key flags next (DeleteItemConfirm Comp.YesNoDimmer.activate) model
SetCorrOrgSuggestion idname ->
( model, setCorrOrg flags model (Just idname) )
@ -943,6 +948,37 @@ update key flags next msg model =
, Cmd.none
)
DeleteAttachConfirm attachId lmsg ->
let
( cm, confirmed ) =
Comp.YesNoDimmer.update lmsg model.deleteAttachConfirm
cmd =
if confirmed then
Api.deleteAttachment flags attachId DeleteAttachResp
else
Cmd.none
in
( { model | deleteAttachConfirm = cm }, cmd )
DeleteAttachResp (Ok res) ->
if res.success then
update key flags next ReloadItem model
else
( model, Cmd.none )
DeleteAttachResp (Err _) ->
( model, Cmd.none )
RequestDeleteAttachment id ->
update key
flags
next
(DeleteAttachConfirm id Comp.YesNoDimmer.activate)
model
-- view
@ -1017,7 +1053,7 @@ view inav model =
]
, renderMailForm model
, div [ class "ui grid" ]
[ Html.map YesNoMsg (Comp.YesNoDimmer.view model.deleteConfirm)
[ Html.map DeleteItemConfirm (Comp.YesNoDimmer.view model.deleteItemConfirm)
, div
[ classList
[ ( "four wide column", True )
@ -1206,7 +1242,8 @@ renderAttachmentView model pos attach =
, ( "active", attachmentVisible model pos )
]
]
[ div [ class "ui small secondary menu" ]
[ Html.map (DeleteAttachConfirm attach.id) (Comp.YesNoDimmer.view model.deleteAttachConfirm)
, div [ class "ui small secondary menu" ]
[ div [ class "horizontally fitted item" ]
[ i [ class "file outline icon" ] []
, text attachName
@ -1227,6 +1264,16 @@ renderAttachmentView model pos attach =
]
, div [ class "right menu" ]
[ a
[ classList
[ ( "item", True )
]
, title "Delete this file permanently"
, href "#"
, onClick (RequestDeleteAttachment attach.id)
]
[ i [ class "red trash icon" ] []
]
, a
[ classList
[ ( "item", True )
, ( "invisible", not hasArchive )