From fa3431202052c5f03f58262c55b9601bdc121427 Mon Sep 17 00:00:00 2001
From: Stefan Scheidewig <stefan.scheidewig@staffbase.com>
Date: Thu, 15 Apr 2021 18:05:01 +0200
Subject: [PATCH] Implemented endpoint to delete multiple attachments

---
 .../scala/docspell/backend/ops/OItem.scala    | 27 ++++++++++---
 .../docspell/restserver/RestServer.scala      |  1 +
 .../routes/AttachmentMultiRoutes.scala        | 38 +++++++++++++++++++
 .../docspell/store/records/RAttachment.scala  | 24 ++++++++----
 4 files changed, 77 insertions(+), 13 deletions(-)
 create mode 100644 modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala

diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala
index 53acd38d..5d8a6143 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala
@@ -1,19 +1,15 @@
 package docspell.backend.ops
 
-import cats.data.NonEmptyList
-import cats.data.OptionT
+import cats.data.{NonEmptyList, OptionT}
 import cats.effect.{Effect, Resource}
 import cats.implicits._
-
 import docspell.backend.JobFactory
 import docspell.common._
 import docspell.ftsclient.FtsClient
-import docspell.store.UpdateResult
 import docspell.store.queries.{QAttachment, QItem, QMoveAttachment}
 import docspell.store.queue.JobQueue
 import docspell.store.records._
-import docspell.store.{AddResult, Store}
-
+import docspell.store.{AddResult, Store, UpdateResult}
 import doobie.implicits._
 import org.log4s.getLogger
 
@@ -140,6 +136,11 @@ trait OItem[F[_]] {
 
   def deleteAttachment(id: Ident, collective: Ident): F[Int]
 
+  def deleteAttachmentMultiple(
+      attachments: NonEmptyList[Ident],
+      collective: Ident
+  ): F[Int]
+
   def moveAttachmentBefore(itemId: Ident, source: Ident, target: Ident): F[AddResult]
 
   def setAttachmentName(
@@ -602,6 +603,20 @@ object OItem {
             .deleteSingleAttachment(store)(id, collective)
             .flatTap(_ => fts.removeAttachment(logger, id))
 
+        def deleteAttachmentMultiple(
+            attachments: NonEmptyList[Ident],
+            collective: Ident
+        ): F[Int] =
+          for {
+            attachmentIds <- store.transact(
+              RAttachment.filterAttachments(attachments, collective)
+            )
+            results <- attachmentIds.traverse(attachment =>
+              deleteAttachment(attachment, collective)
+            )
+            n = results.sum
+          } yield n
+
         def setAttachmentName(
             attachId: Ident,
             name: Option[String],
diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala
index 7fc0cdfb..30e5733e 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala
@@ -80,6 +80,7 @@ object RestServer {
       "item"                    -> ItemRoutes(cfg, pools.blocker, restApp.backend, token),
       "items"                   -> ItemMultiRoutes(restApp.backend, token),
       "attachment"              -> AttachmentRoutes(pools.blocker, restApp.backend, token),
+      "attachments"             -> AttachmentMultiRoutes(restApp.backend, token),
       "upload"                  -> UploadRoutes.secured(restApp.backend, cfg, token),
       "checkfile"               -> CheckFileRoutes.secured(restApp.backend, token),
       "email/send"              -> MailSendRoutes(restApp.backend, token),
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala
new file mode 100644
index 00000000..fede7636
--- /dev/null
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentMultiRoutes.scala
@@ -0,0 +1,38 @@
+package docspell.restserver.routes
+
+import cats.effect.Effect
+import cats.implicits._
+import docspell.backend.BackendApp
+import docspell.backend.auth.AuthToken
+import docspell.restapi.model._
+import docspell.restserver.conv.MultiIdSupport
+import org.http4s.HttpRoutes
+import org.http4s.circe.CirceEntityDecoder._
+import org.http4s.circe.CirceEntityEncoder._
+import org.http4s.dsl.Http4sDsl
+
+object AttachmentMultiRoutes extends MultiIdSupport {
+
+  def apply[F[_]: Effect](
+      backend: BackendApp[F],
+      user: AuthToken
+  ): HttpRoutes[F] = {
+
+    val dsl = new Http4sDsl[F] {}
+    import dsl._
+
+    HttpRoutes.of { case req @ POST -> Root / "delete" =>
+      for {
+        json        <- req.as[IdList]
+        attachments <- readIds[F](json.ids)
+        n           <- backend.item.deleteAttachmentMultiple(attachments, user.account.collective)
+        res = BasicResult(
+          n > 0,
+          if (n > 0) "Attachment(s) deleted" else "Attachment deletion failed."
+        )
+        resp <- Ok(res)
+      } yield resp
+    }
+  }
+
+}
diff --git a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala
index e781eb89..ec892df7 100644
--- a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala
@@ -1,16 +1,14 @@
 package docspell.store.records
 
+import bitpeace.FileMeta
 import cats.data.NonEmptyList
 import cats.implicits._
-import fs2.Stream
-
 import docspell.common._
 import docspell.store.qb.DSL._
 import docspell.store.qb._
-
-import bitpeace.FileMeta
 import doobie._
 import doobie.implicits._
+import fs2.Stream
 
 case class RAttachment(
     id: Ident,
@@ -98,7 +96,6 @@ object RAttachment {
     run(select(T.all), from(T), T.id === attachId).query[RAttachment].option
 
   def findMeta(attachId: Ident): ConnectionIO[Option[FileMeta]] = {
-    import bitpeace.sql._
 
     val m = RFileMeta.as("m")
     val a = RAttachment.as("a")
@@ -191,7 +188,6 @@ object RAttachment {
       id: Ident,
       coll: Ident
   ): ConnectionIO[Vector[(RAttachment, FileMeta)]] = {
-    import bitpeace.sql._
 
     val a = RAttachment.as("a")
     val m = RFileMeta.as("m")
@@ -206,7 +202,6 @@ object RAttachment {
   }
 
   def findByItemWithMeta(id: Ident): ConnectionIO[Vector[(RAttachment, FileMeta)]] = {
-    import bitpeace.sql._
 
     val a = RAttachment.as("a")
     val m = RFileMeta.as("m")
@@ -301,4 +296,19 @@ object RAttachment {
         coll.map(cid => i.cid === cid)
     ).build.query[RAttachment].streamWithChunkSize(chunkSize)
   }
+
+  def filterAttachments(
+      attachments: NonEmptyList[Ident],
+      coll: Ident
+  ): ConnectionIO[Vector[Ident]] = {
+    val a = RAttachment.as("a")
+    val i = RItem.as("i")
+
+    Select(
+      select(a.id),
+      from(a)
+        .innerJoin(i, i.id === a.itemId),
+      i.cid === coll && a.id.in(attachments)
+    ).build.query[Ident].to[Vector]
+  }
 }