From e52271f9cdbc9e99144e2d36f94baa1f01996d32 Mon Sep 17 00:00:00 2001 From: eikek Date: Tue, 5 Oct 2021 09:24:11 +0200 Subject: [PATCH] Implement share preview image --- .../scala/docspell/backend/BackendApp.scala | 2 +- .../scala/docspell/backend/ops/OShare.scala | 43 +++++++++++++++++-- .../docspell/restserver/RestServer.scala | 3 +- .../restserver/http4s/BinaryUtil.scala | 34 ++++++++++++++- .../restserver/routes/AttachmentRoutes.scala | 19 +------- .../restserver/routes/ItemRoutes.scala | 2 +- .../routes/ShareAttachmentRoutes.scala | 37 ++++++++++++++++ .../restserver/routes/ShareSearchRoutes.scala | 2 +- 8 files changed, 115 insertions(+), 27 deletions(-) create mode 100644 modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala index 5a6fa482..1e58654c 100644 --- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala +++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala @@ -86,7 +86,7 @@ object BackendApp { customFieldsImpl <- OCustomFields(store) simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl) clientSettingsImpl <- OClientSettings(store) - shareImpl <- Resource.pure(OShare(store)) + shareImpl <- Resource.pure(OShare(store, itemSearchImpl)) } yield new BackendApp[F] { val login = loginImpl val signup = signupImpl 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 d0b82171..77b882f2 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -9,15 +9,15 @@ package docspell.backend.ops import cats.data.OptionT import cats.effect._ import cats.implicits._ - import docspell.backend.PasswordCrypt import docspell.backend.auth.ShareToken +import docspell.backend.ops.OItemSearch.{AttachmentPreviewData, Batch, Query} import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} import docspell.common._ import docspell.query.ItemQuery +import docspell.query.ItemQuery.Expr.AttachId import docspell.store.Store import docspell.store.records.RShare - import scodec.bits.ByteVector trait OShare[F[_]] { @@ -45,10 +45,21 @@ trait OShare[F[_]] { def verifyToken(key: ByteVector)(token: String): F[VerifyResult] def findShareQuery(id: Ident): OptionT[F, ShareQuery] + + def findAttachmentPreview( + attachId: Ident, + shareId: Ident + ): OptionT[F, AttachmentPreviewData[F]] + } object OShare { - final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery) + final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery) { + + //TODO + def asAccount: AccountId = + AccountId(cid, Ident.unsafe("")) + } sealed trait VerifyResult { def toEither: Either[String, ShareToken] = @@ -90,7 +101,7 @@ object OShare { def publishUntilInPast: ChangeResult = PublishUntilInPast } - def apply[F[_]: Async](store: Store[F]): OShare[F] = + def apply[F[_]: Async](store: Store[F], itemSearch: OItemSearch[F]): OShare[F] = new OShare[F] { private[this] val logger = Logger.log4s[F](org.log4s.getLogger) @@ -203,5 +214,29 @@ object OShare { .findCurrentActive(id) .mapK(store.transform) .map(share => ShareQuery(share.id, share.cid, share.query)) + + def findAttachmentPreview( + attachId: Ident, + shareId: Ident + ): OptionT[F, AttachmentPreviewData[F]] = + for { + sq <- findShareQuery(shareId) + account = sq.asAccount + checkQuery = Query( + Query.Fix(account, Some(sq.query.expr), None), + Query.QueryExpr(AttachId(attachId.id)) + ) + checkRes <- OptionT.liftF(itemSearch.findItems(0)(checkQuery, Batch.limit(1))) + res <- + if (checkRes.isEmpty) + OptionT + .liftF( + logger.info( + s"Attempt to load unshared attachment '${attachId.id}' for share: ${shareId.id}" + ) + ) + .mapFilter(_ => None) + else OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid)) + } yield res } } diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index cceacfdd..2e66c65f 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -142,7 +142,8 @@ object RestServer { token: ShareToken ): HttpRoutes[F] = Router( - "search" -> ShareSearchRoutes(restApp.backend, cfg, token) + "search" -> ShareSearchRoutes(restApp.backend, cfg, token), + "attachment" -> ShareAttachmentRoutes(restApp.backend, token) ) def redirectTo[F[_]: Async](path: String): HttpRoutes[F] = { diff --git a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala index e97eda8f..91baf47e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala @@ -10,19 +10,49 @@ import cats.data.NonEmptyList import cats.data.OptionT import cats.effect._ import cats.implicits._ - +import docspell.backend.ops.OItemSearch.AttachmentPreviewData import docspell.backend.ops._ +import docspell.restapi.model.BasicResult import docspell.store.records.RFileMeta +import docspell.restserver.http4s.{QueryParam => QP} import org.http4s._ import org.http4s.circe.CirceEntityEncoder._ import org.http4s.dsl.Http4sDsl -import org.http4s.headers.ETag.EntityTag import org.http4s.headers._ +import org.http4s.headers.ETag.EntityTag import org.typelevel.ci.CIString object BinaryUtil { + def respond[F[_]: Async](dsl: Http4sDsl[F], req: Request[F])( + fileData: Option[AttachmentPreviewData[F]] + ): F[Response[F]] = { + import dsl._ + def notFound = + NotFound(BasicResult(false, "Not found")) + + QP.WithFallback.unapply(req.multiParams) match { + case Some(bool) => + val fallback = bool.getOrElse(false) + val inm = req.headers.get[`If-None-Match`].flatMap(_.tags) + val matches = matchETag(fileData.map(_.meta), inm) + + fileData + .map { data => + if (matches) withResponseHeaders(dsl, NotModified())(data) + else makeByteResp(dsl)(data) + } + .getOrElse( + if (fallback) BinaryUtil.noPreview(req.some).getOrElseF(notFound) + else notFound + ) + + case None => + BadRequest(BasicResult(false, "Invalid query parameter 'withFallback'")) + } + } + def withResponseHeaders[F[_]: Sync](dsl: Http4sDsl[F], resp: F[Response[F]])( data: OItemSearch.BinaryData[F] ): F[Response[F]] = { 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 9b5d52aa..63b818cd 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala @@ -17,7 +17,6 @@ import docspell.common.MakePreviewArgs import docspell.restapi.model._ import docspell.restserver.conv.Conversions import docspell.restserver.http4s.BinaryUtil -import docspell.restserver.http4s.{QueryParam => QP} import docspell.restserver.webapp.Webjars import org.http4s._ @@ -115,25 +114,11 @@ object AttachmentRoutes { .getOrElse(NotFound(BasicResult(false, "Not found"))) } yield resp - case req @ GET -> Root / Ident(id) / "preview" :? QP.WithFallback(flag) => - def notFound = - NotFound(BasicResult(false, "Not found")) + case req @ GET -> Root / Ident(id) / "preview" => for { fileData <- backend.itemSearch.findAttachmentPreview(id, user.account.collective) - inm = req.headers.get[`If-None-Match`].flatMap(_.tags) - matches = BinaryUtil.matchETag(fileData.map(_.meta), inm) - fallback = flag.getOrElse(false) - resp <- - fileData - .map { data => - if (matches) withResponseHeaders(NotModified())(data) - else makeByteResp(data) - } - .getOrElse( - if (fallback) BinaryUtil.noPreview(req.some).getOrElseF(notFound) - else notFound - ) + resp <- BinaryUtil.respond(dsl, req)(fileData) } yield resp case HEAD -> Root / Ident(id) / "preview" => diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala index 8ea503bd..5277d0c8 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala @@ -452,7 +452,7 @@ object ItemRoutes { } } - private def searchItemStats[F[_]: Sync]( + def searchItemStats[F[_]: Sync]( backend: BackendApp[F], dsl: Http4sDsl[F] )( diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala new file mode 100644 index 00000000..af02bf16 --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala @@ -0,0 +1,37 @@ +/* + * 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.restserver.http4s.BinaryUtil + +import org.http4s.HttpRoutes +import org.http4s.dsl.Http4sDsl + +object ShareAttachmentRoutes { + + def apply[F[_]: Async]( + backend: BackendApp[F], + token: ShareToken + ): HttpRoutes[F] = { + val dsl = new Http4sDsl[F] {} + import dsl._ + + HttpRoutes.of { case req @ GET -> Root / Ident(id) / "preview" => + for { + fileData <- + backend.share.findAttachmentPreview(id, token.id).value + resp <- BinaryUtil.respond(dsl, req)(fileData) + } yield resp + } + } +} diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala index a0ffa3a9..39a45412 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala @@ -54,7 +54,7 @@ object ShareSearchRoutes { cfg.maxNoteLength, searchMode = SearchMode.Normal ) - account = AccountId(share.cid, Ident.unsafe("")) + account = share.asAccount fixQuery = Query.Fix(account, Some(share.query.expr), None) _ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}") resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery)