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] + } }