diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index 75e899a6..57a2c236 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -17,6 +17,7 @@ import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} import docspell.backend.ops.OSimpleSearch.StringSearchResult import docspell.common._ import docspell.query.ItemQuery +import docspell.query.ItemQuery.Expr import docspell.query.ItemQuery.Expr.AttachId import docspell.store.Store import docspell.store.queries.SearchSummary @@ -57,6 +58,8 @@ trait OShare[F[_]] { def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] + def findItem(itemId: Ident, shareId: Ident): OptionT[F, ItemData] + def searchSummary( settings: OSimpleSearch.StatsSettings )(shareId: Ident, q: ItemQueryString): OptionT[F, StringSearchResult[SearchSummary]] @@ -234,24 +237,31 @@ object OShare { ): OptionT[F, AttachmentPreviewData[F]] = for { sq <- findShareQuery(shareId) - _ <- checkAttachment(sq, attachId) + _ <- checkAttachment(sq, AttachId(attachId.id)) res <- OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid)) } yield res def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] = for { sq <- findShareQuery(shareId) - _ <- checkAttachment(sq, attachId) + _ <- checkAttachment(sq, AttachId(attachId.id)) res <- OptionT(itemSearch.findAttachment(attachId, sq.cid)) } yield res + def findItem(itemId: Ident, shareId: Ident): OptionT[F, ItemData] = + for { + sq <- findShareQuery(shareId) + _ <- checkAttachment(sq, Expr.itemIdEq(itemId.id)) + res <- OptionT(itemSearch.findItem(itemId, sq.cid)) + } yield res + /** Check whether the attachment with the given id is in the results of the given * share */ - private def checkAttachment(sq: ShareQuery, attachId: Ident): OptionT[F, Unit] = { + private def checkAttachment(sq: ShareQuery, idExpr: Expr): OptionT[F, Unit] = { val checkQuery = Query( Query.Fix(sq.asAccount, Some(sq.query.expr), None), - Query.QueryExpr(AttachId(attachId.id)) + Query.QueryExpr(idExpr) ) OptionT( itemSearch @@ -259,7 +269,7 @@ object OShare { .map(_.headOption.map(_ => ())) ).flatTapNone( logger.info( - s"Attempt to load unshared attachment '${attachId.id}' via share: ${sq.id.id}" + s"Attempt to load unshared data '$idExpr' via share: ${sq.id.id}" ) ) } diff --git a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala index be0e5135..c9466ac0 100644 --- a/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala +++ b/modules/query/shared/src/main/scala/docspell/query/ItemQuery.scala @@ -188,6 +188,10 @@ object ItemQuery { def date(op: Operator, attr: DateAttr, value: Date): SimpleExpr = SimpleExpr(op, Property(attr, value)) + + def itemIdEq(itemId1: String, moreIds: String*): Expr = + if (moreIds.isEmpty) string(Operator.Eq, Attr.ItemId, itemId1) + else InExpr(Attr.ItemId, Nel(itemId1, moreIds.toList)) } } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index 0c0dd351..8745c23c 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1603,6 +1603,97 @@ paths: application/json: schema: $ref: "#/components/schemas/SearchStats" + /share/item/{id}: + get: + operationId: "share-item-get" + tags: [ Share ] + summary: Get details about an item. + description: | + Get detailed information about an item. + security: + - shareTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/ItemDetail" + /share/attachment/{id}: + head: + operationId: "share-attach-head" + tags: [ Share ] + summary: Get headers to an attachment file. + description: | + Get information about the binary file belonging to the + attachment with the given id. + security: + - shareTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + headers: + Content-Type: + schema: + type: string + Content-Length: + schema: + type: integer + format: int64 + ETag: + schema: + type: string + Content-Disposition: + schema: + type: string + get: + operationId: "share-attach-get" + tags: [ Share ] + summary: Get an attachment file. + description: | + Get the binary file belonging to the attachment with the given + id. The binary is a pdf file. If conversion failed, then the + original file is returned. + security: + - shareTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + content: + application/octet-stream: + schema: + type: string + format: binary + /share/attachment/{id}/view: + get: + operationId: "share-attach-show-viewerjs" + tags: [ Share ] + summary: A javascript rendered view of the pdf attachment + description: | + This provides a preview of the attachment rendered in a + browser. + + It currently uses a third-party javascript library (viewerjs) + to display the preview. This works by redirecting to the + viewerjs url with the attachment url as parameter. Note that + the resulting url that is redirected to is not stable. It may + change from version to version. This route, however, is meant + to provide a stable url for the preview. + security: + - shareTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 303: + description: See Other + 200: + description: Ok /share/attachment/{id}/preview: head: operationId: "share-attach-check-preview" diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index 2e66c65f..9881fdcc 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -143,7 +143,8 @@ object RestServer { ): HttpRoutes[F] = Router( "search" -> ShareSearchRoutes(restApp.backend, cfg, token), - "attachment" -> ShareAttachmentRoutes(restApp.backend, token) + "attachment" -> ShareAttachmentRoutes(restApp.backend, token), + "item" -> ShareItemRoutes(restApp.backend, token) ) def redirectTo[F[_]: Async](path: String): HttpRoutes[F] = { diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareItemRoutes.scala new file mode 100644 index 00000000..38c3d041 --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareItemRoutes.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package docspell.restserver.routes +import cats.effect._ +import cats.implicits._ + +import docspell.backend.BackendApp +import docspell.backend.auth.ShareToken +import docspell.common._ +import docspell.restapi.model.BasicResult +import docspell.restserver.conv.Conversions + +import org.http4s._ +import org.http4s.circe.CirceEntityEncoder._ +import org.http4s.dsl.Http4sDsl + +object ShareItemRoutes { + + def apply[F[_]: Async]( + backend: BackendApp[F], + token: ShareToken + ): HttpRoutes[F] = { + val dsl = new Http4sDsl[F] {} + import dsl._ + + HttpRoutes.of { case GET -> Root / Ident(id) => + for { + item <- backend.share.findItem(id, token.id).value + result = item.map(Conversions.mkItemDetail) + resp <- + result + .map(r => Ok(r)) + .getOrElse(NotFound(BasicResult(false, "Not found."))) + } yield resp + } + } +}