mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-02 13:32:51 +00:00
Delete single attachments
This commit is contained in:
parent
916a9dbcc9
commit
a939839041
@ -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
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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 )
|
||||
|
Loading…
x
Reference in New Issue
Block a user