Implement share preview image

This commit is contained in:
eikek 2021-10-05 09:24:11 +02:00
parent 7b0f378558
commit e52271f9cd
8 changed files with 115 additions and 27 deletions

View File

@ -86,7 +86,7 @@ object BackendApp {
customFieldsImpl <- OCustomFields(store) customFieldsImpl <- OCustomFields(store)
simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl) simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl)
clientSettingsImpl <- OClientSettings(store) clientSettingsImpl <- OClientSettings(store)
shareImpl <- Resource.pure(OShare(store)) shareImpl <- Resource.pure(OShare(store, itemSearchImpl))
} yield new BackendApp[F] { } yield new BackendApp[F] {
val login = loginImpl val login = loginImpl
val signup = signupImpl val signup = signupImpl

View File

@ -9,15 +9,15 @@ package docspell.backend.ops
import cats.data.OptionT import cats.data.OptionT
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
import docspell.backend.PasswordCrypt import docspell.backend.PasswordCrypt
import docspell.backend.auth.ShareToken import docspell.backend.auth.ShareToken
import docspell.backend.ops.OItemSearch.{AttachmentPreviewData, Batch, Query}
import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} import docspell.backend.ops.OShare.{ShareQuery, VerifyResult}
import docspell.common._ import docspell.common._
import docspell.query.ItemQuery import docspell.query.ItemQuery
import docspell.query.ItemQuery.Expr.AttachId
import docspell.store.Store import docspell.store.Store
import docspell.store.records.RShare import docspell.store.records.RShare
import scodec.bits.ByteVector import scodec.bits.ByteVector
trait OShare[F[_]] { trait OShare[F[_]] {
@ -45,10 +45,21 @@ trait OShare[F[_]] {
def verifyToken(key: ByteVector)(token: String): F[VerifyResult] def verifyToken(key: ByteVector)(token: String): F[VerifyResult]
def findShareQuery(id: Ident): OptionT[F, ShareQuery] def findShareQuery(id: Ident): OptionT[F, ShareQuery]
def findAttachmentPreview(
attachId: Ident,
shareId: Ident
): OptionT[F, AttachmentPreviewData[F]]
} }
object OShare { 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 { sealed trait VerifyResult {
def toEither: Either[String, ShareToken] = def toEither: Either[String, ShareToken] =
@ -90,7 +101,7 @@ object OShare {
def publishUntilInPast: ChangeResult = PublishUntilInPast 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] { new OShare[F] {
private[this] val logger = Logger.log4s[F](org.log4s.getLogger) private[this] val logger = Logger.log4s[F](org.log4s.getLogger)
@ -203,5 +214,29 @@ object OShare {
.findCurrentActive(id) .findCurrentActive(id)
.mapK(store.transform) .mapK(store.transform)
.map(share => ShareQuery(share.id, share.cid, share.query)) .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
} }
} }

View File

@ -142,7 +142,8 @@ object RestServer {
token: ShareToken token: ShareToken
): HttpRoutes[F] = ): HttpRoutes[F] =
Router( 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] = { def redirectTo[F[_]: Async](path: String): HttpRoutes[F] = {

View File

@ -10,19 +10,49 @@ import cats.data.NonEmptyList
import cats.data.OptionT import cats.data.OptionT
import cats.effect._ import cats.effect._
import cats.implicits._ import cats.implicits._
import docspell.backend.ops.OItemSearch.AttachmentPreviewData
import docspell.backend.ops._ import docspell.backend.ops._
import docspell.restapi.model.BasicResult
import docspell.store.records.RFileMeta import docspell.store.records.RFileMeta
import docspell.restserver.http4s.{QueryParam => QP}
import org.http4s._ import org.http4s._
import org.http4s.circe.CirceEntityEncoder._ import org.http4s.circe.CirceEntityEncoder._
import org.http4s.dsl.Http4sDsl import org.http4s.dsl.Http4sDsl
import org.http4s.headers.ETag.EntityTag
import org.http4s.headers._ import org.http4s.headers._
import org.http4s.headers.ETag.EntityTag
import org.typelevel.ci.CIString import org.typelevel.ci.CIString
object BinaryUtil { 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]])( def withResponseHeaders[F[_]: Sync](dsl: Http4sDsl[F], resp: F[Response[F]])(
data: OItemSearch.BinaryData[F] data: OItemSearch.BinaryData[F]
): F[Response[F]] = { ): F[Response[F]] = {

View File

@ -17,7 +17,6 @@ import docspell.common.MakePreviewArgs
import docspell.restapi.model._ import docspell.restapi.model._
import docspell.restserver.conv.Conversions import docspell.restserver.conv.Conversions
import docspell.restserver.http4s.BinaryUtil import docspell.restserver.http4s.BinaryUtil
import docspell.restserver.http4s.{QueryParam => QP}
import docspell.restserver.webapp.Webjars import docspell.restserver.webapp.Webjars
import org.http4s._ import org.http4s._
@ -115,25 +114,11 @@ object AttachmentRoutes {
.getOrElse(NotFound(BasicResult(false, "Not found"))) .getOrElse(NotFound(BasicResult(false, "Not found")))
} yield resp } yield resp
case req @ GET -> Root / Ident(id) / "preview" :? QP.WithFallback(flag) => case req @ GET -> Root / Ident(id) / "preview" =>
def notFound =
NotFound(BasicResult(false, "Not found"))
for { for {
fileData <- fileData <-
backend.itemSearch.findAttachmentPreview(id, user.account.collective) backend.itemSearch.findAttachmentPreview(id, user.account.collective)
inm = req.headers.get[`If-None-Match`].flatMap(_.tags) resp <- BinaryUtil.respond(dsl, req)(fileData)
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
)
} yield resp } yield resp
case HEAD -> Root / Ident(id) / "preview" => case HEAD -> Root / Ident(id) / "preview" =>

View File

@ -452,7 +452,7 @@ object ItemRoutes {
} }
} }
private def searchItemStats[F[_]: Sync]( def searchItemStats[F[_]: Sync](
backend: BackendApp[F], backend: BackendApp[F],
dsl: Http4sDsl[F] dsl: Http4sDsl[F]
)( )(

View File

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

View File

@ -54,7 +54,7 @@ object ShareSearchRoutes {
cfg.maxNoteLength, cfg.maxNoteLength,
searchMode = SearchMode.Normal searchMode = SearchMode.Normal
) )
account = AccountId(share.cid, Ident.unsafe("")) account = share.asAccount
fixQuery = Query.Fix(account, Some(share.query.expr), None) fixQuery = Query.Fix(account, Some(share.query.expr), None)
_ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}") _ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}")
resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery) resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery)