From 1a10216e3d8821bfb97392026f47b9016375164c Mon Sep 17 00:00:00 2001
From: eikek <eike.kettner@posteo.de>
Date: Wed, 6 Oct 2021 09:36:38 +0200
Subject: [PATCH] Get item details from a share

---
 .../scala/docspell/backend/ops/OShare.scala   | 20 +++-
 .../main/scala/docspell/query/ItemQuery.scala |  4 +
 .../src/main/resources/docspell-openapi.yml   | 91 +++++++++++++++++++
 .../docspell/restserver/RestServer.scala      |  3 +-
 .../restserver/routes/ShareItemRoutes.scala   | 41 +++++++++
 5 files changed, 153 insertions(+), 6 deletions(-)
 create mode 100644 modules/restserver/src/main/scala/docspell/restserver/routes/ShareItemRoutes.scala

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