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 0f064c36..75e899a6 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -12,7 +12,7 @@ import cats.implicits._ import docspell.backend.PasswordCrypt import docspell.backend.auth.ShareToken -import docspell.backend.ops.OItemSearch.{AttachmentPreviewData, Batch, Query} +import docspell.backend.ops.OItemSearch._ import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} import docspell.backend.ops.OSimpleSearch.StringSearchResult import docspell.common._ @@ -55,6 +55,8 @@ trait OShare[F[_]] { shareId: Ident ): OptionT[F, AttachmentPreviewData[F]] + def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] + def searchSummary( settings: OSimpleSearch.StatsSettings )(shareId: Ident, q: ItemQueryString): OptionT[F, StringSearchResult[SearchSummary]] @@ -232,24 +234,36 @@ object OShare { ): 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)) + _ <- checkAttachment(sq, attachId) + 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) + res <- OptionT(itemSearch.findAttachment(attachId, 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] = { + val checkQuery = Query( + Query.Fix(sq.asAccount, Some(sq.query.expr), None), + Query.QueryExpr(AttachId(attachId.id)) + ) + OptionT( + itemSearch + .findItems(0)(checkQuery, Batch.limit(1)) + .map(_.headOption.map(_ => ())) + ).flatTapNone( + logger.info( + s"Attempt to load unshared attachment '${attachId.id}' via share: ${sq.id.id}" + ) + ) + } + def searchSummary( settings: OSimpleSearch.StatsSettings )( diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala index be89e955..03142eaf 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala @@ -7,9 +7,11 @@ package docspell.restserver.conv import java.time.{LocalDate, ZoneId} + import cats.effect.{Async, Sync} import cats.implicits._ import fs2.Stream + import docspell.backend.ops.OCollective.{InsightData, PassChangeResult} import docspell.backend.ops.OCustomFields.SetValueResult import docspell.backend.ops.OJob.JobCancelResult @@ -23,6 +25,7 @@ import docspell.restserver.conv.Conversions._ import docspell.store.queries.{AttachmentLight => QAttachmentLight, IdRefCount} import docspell.store.records._ import docspell.store.{AddResult, UpdateResult} + import org.http4s.headers.`Content-Type` import org.http4s.multipart.Multipart import org.log4s.Logger 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 208847a6..7ebdb9b3 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/http4s/BinaryUtil.scala @@ -11,7 +11,7 @@ import cats.data.OptionT import cats.effect._ import cats.implicits._ -import docspell.backend.ops.OItemSearch.AttachmentPreviewData +import docspell.backend.ops.OItemSearch.{AttachmentData, AttachmentPreviewData} import docspell.backend.ops._ import docspell.restapi.model.BasicResult import docspell.restserver.http4s.{QueryParam => QP} @@ -27,6 +27,31 @@ import org.typelevel.ci.CIString object BinaryUtil { def respond[F[_]: Async](dsl: Http4sDsl[F], req: Request[F])( + fileData: Option[AttachmentData[F]] + ): F[Response[F]] = { + import dsl._ + + val inm = req.headers.get[`If-None-Match`].flatMap(_.tags) + val matches = BinaryUtil.matchETag(fileData.map(_.meta), inm) + fileData + .map { data => + if (matches) withResponseHeaders(dsl, NotModified())(data) + else makeByteResp(dsl)(data) + } + .getOrElse(NotFound(BasicResult(false, "Not found"))) + } + + def respondHead[F[_]: Async](dsl: Http4sDsl[F])( + fileData: Option[AttachmentData[F]] + ): F[Response[F]] = { + import dsl._ + + fileData + .map(data => withResponseHeaders(dsl, Ok())(data)) + .getOrElse(NotFound(BasicResult(false, "Not found"))) + } + + def respondPreview[F[_]: Async](dsl: Http4sDsl[F], req: Request[F])( fileData: Option[AttachmentPreviewData[F]] ): F[Response[F]] = { import dsl._ @@ -54,7 +79,7 @@ object BinaryUtil { } } - def respondHead[F[_]: Async]( + def respondPreviewHead[F[_]: Async]( dsl: Http4sDsl[F] )(fileData: Option[AttachmentPreviewData[F]]): F[Response[F]] = { import dsl._ 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 cf4e23f7..a7bc4b82 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala @@ -46,24 +46,13 @@ object AttachmentRoutes { case HEAD -> Root / Ident(id) => for { fileData <- backend.itemSearch.findAttachment(id, user.account.collective) - resp <- - fileData - .map(data => withResponseHeaders(Ok())(data)) - .getOrElse(NotFound(BasicResult(false, "Not found"))) + resp <- BinaryUtil.respondHead(dsl)(fileData) } yield resp case req @ GET -> Root / Ident(id) => for { fileData <- backend.itemSearch.findAttachment(id, user.account.collective) - inm = req.headers.get[`If-None-Match`].flatMap(_.tags) - matches = BinaryUtil.matchETag(fileData.map(_.meta), inm) - resp <- - fileData - .map { data => - if (matches) withResponseHeaders(NotModified())(data) - else makeByteResp(data) - } - .getOrElse(NotFound(BasicResult(false, "Not found"))) + resp <- BinaryUtil.respond[F](dsl, req)(fileData) } yield resp case HEAD -> Root / Ident(id) / "original" => @@ -118,14 +107,14 @@ object AttachmentRoutes { for { fileData <- backend.itemSearch.findAttachmentPreview(id, user.account.collective) - resp <- BinaryUtil.respond(dsl, req)(fileData) + resp <- BinaryUtil.respondPreview(dsl, req)(fileData) } yield resp case HEAD -> Root / Ident(id) / "preview" => for { fileData <- backend.itemSearch.findAttachmentPreview(id, user.account.collective) - resp <- BinaryUtil.respondHead(dsl)(fileData) + resp <- BinaryUtil.respondPreviewHead(dsl)(fileData) } yield resp case POST -> Root / Ident(id) / "preview" => diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala index d763530b..b93a1381 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareAttachmentRoutes.scala @@ -13,9 +13,11 @@ import docspell.backend.BackendApp import docspell.backend.auth.ShareToken import docspell.common._ import docspell.restserver.http4s.BinaryUtil +import docspell.restserver.webapp.Webjars -import org.http4s.HttpRoutes +import org.http4s._ import org.http4s.dsl.Http4sDsl +import org.http4s.headers._ object ShareAttachmentRoutes { @@ -27,18 +29,35 @@ object ShareAttachmentRoutes { import dsl._ HttpRoutes.of { + case HEAD -> Root / Ident(id) => + for { + fileData <- backend.share.findAttachment(id, token.id).value + resp <- BinaryUtil.respondHead(dsl)(fileData) + } yield resp + + case req @ GET -> Root / Ident(id) => + for { + fileData <- backend.share.findAttachment(id, token.id).value + resp <- BinaryUtil.respond(dsl, req)(fileData) + } yield resp + + case GET -> Root / Ident(id) / "view" => + // this route exists to provide a stable url + // it redirects currently to viewerjs + val attachUrl = s"/api/v1/share/attachment/${id.id}" + val path = s"/app/assets${Webjars.viewerjs}/index.html#$attachUrl" + SeeOther(Location(Uri(path = Uri.Path.unsafeFromString(path)))) + case req @ GET -> Root / Ident(id) / "preview" => for { - fileData <- - backend.share.findAttachmentPreview(id, token.id).value - resp <- BinaryUtil.respond(dsl, req)(fileData) + fileData <- backend.share.findAttachmentPreview(id, token.id).value + resp <- BinaryUtil.respondPreview(dsl, req)(fileData) } yield resp case HEAD -> Root / Ident(id) / "preview" => for { - fileData <- - backend.share.findAttachmentPreview(id, token.id).value - resp <- BinaryUtil.respondHead(dsl)(fileData) + fileData <- backend.share.findAttachmentPreview(id, token.id).value + resp <- BinaryUtil.respondPreviewHead(dsl)(fileData) } yield resp } } diff --git a/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala b/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala index 20c2fbdf..5ac6682d 100644 --- a/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala +++ b/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala @@ -1,3 +1,9 @@ +/* + * Copyright 2020 Eike K. & Contributors + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + package docspell.store.queries import docspell.common._