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 607dc3a3..fc743aad 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItem.scala @@ -94,6 +94,12 @@ trait OItem[F[_]] { def deleteAttachment(id: Ident, collective: Ident): F[Int] def moveAttachmentBefore(itemId: Ident, source: Ident, target: Ident): F[AddResult] + + def setAttachmentName( + attachId: Ident, + name: Option[String], + collective: Ident + ): F[AddResult] } object OItem { @@ -472,6 +478,16 @@ object OItem { def deleteAttachment(id: Ident, collective: Ident): F[Int] = QAttachment.deleteSingleAttachment(store)(id, collective) + + def setAttachmentName( + attachId: Ident, + name: Option[String], + collective: Ident + ): F[AddResult] = + store + .transact(RAttachment.updateName(attachId, collective, name)) + .attempt + .map(AddResult.fromUpdate) }) } yield oitem } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index b2698722..249d8df3 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1683,6 +1683,31 @@ paths: description: See Other 200: description: Ok + /sec/attachment/{id}/name: + post: + tags: [ Attachment ] + summary: Changes the name of an attachment + description: | + Change the name of the attachment with the given id. The + attachment must be part of an item that belongs to the + collective of the current user. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/OptionalText" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/BasicResult" + /sec/queue/state: get: tags: [ Job Queue ] diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala index 37079630..2b241bea 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala @@ -9,6 +9,7 @@ import org.http4s.dsl.Http4sDsl import org.http4s.headers._ import org.http4s.headers.ETag.EntityTag import org.http4s.circe.CirceEntityEncoder._ +import org.http4s.circe.CirceEntityDecoder._ import docspell.backend.BackendApp import docspell.backend.auth.AuthToken import docspell.backend.ops.OItem @@ -126,6 +127,13 @@ object AttachmentRoutes { resp <- md.map(Ok(_)).getOrElse(NotFound(BasicResult(false, "Not found."))) } yield resp + case req @ POST -> Root / Ident(id) / "name" => + for { + nn <- req.as[OptionalText] + res <- backend.item.setAttachmentName(id, nn.text, user.account.collective) + resp <- Ok(Conversions.basicResult(res, "Name updated.")) + } yield resp + case DELETE -> Root / Ident(id) => for { n <- backend.item.deleteAttachment(id, user.account.collective) 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 def33fa6..bc1fa8f4 100644 --- a/modules/store/src/main/scala/docspell/store/records/RAttachment.scala +++ b/modules/store/src/main/scala/docspell/store/records/RAttachment.scala @@ -1,6 +1,7 @@ package docspell.store.records import bitpeace.FileMeta +import cats.implicits._ import doobie._ import doobie.implicits._ import docspell.common._ @@ -89,6 +90,18 @@ object RAttachment { selectSimple(cols, from, cond).query[FileMeta].option } + def updateName( + attachId: Ident, + collective: Ident, + aname: Option[String] + ): ConnectionIO[Int] = { + val update = updateRow(table, id.is(attachId), name.setTo(aname)).update.run + for { + exists <- existsByIdAndCollective(attachId, collective) + n <- if (exists) update else 0.pure[ConnectionIO] + } yield n + } + def findByIdAndCollective( attachId: Ident, collective: Ident @@ -106,6 +119,20 @@ object RAttachment { def findByItem(id: Ident): ConnectionIO[Vector[RAttachment]] = selectSimple(all, table, itemId.is(id)).query[RAttachment].to[Vector] + def existsByIdAndCollective( + attachId: Ident, + collective: Ident + ): ConnectionIO[Boolean] = { + val aId = id.prefix("a") + val aItem = itemId.prefix("a") + val iId = RItem.Columns.id.prefix("i") + val iColl = RItem.Columns.cid.prefix("i") + val from = + table ++ fr"a INNER JOIN" ++ RItem.table ++ fr"i ON" ++ aItem.is(iId) + val cond = and(iColl.is(collective), aId.is(attachId)) + selectCount(id, from, cond).query[Int].unique.map(_ > 0) + } + def findByItemAndCollective( id: Ident, coll: Ident