mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-10-31 09:30:12 +00:00 
			
		
		
		
	Implement binary routes for shares
This commit is contained in:
		| @@ -12,7 +12,7 @@ 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.OItemSearch._ | ||||||
| import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} | import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} | ||||||
| import docspell.backend.ops.OSimpleSearch.StringSearchResult | import docspell.backend.ops.OSimpleSearch.StringSearchResult | ||||||
| import docspell.common._ | import docspell.common._ | ||||||
| @@ -55,6 +55,8 @@ trait OShare[F[_]] { | |||||||
|       shareId: Ident |       shareId: Ident | ||||||
|   ): OptionT[F, AttachmentPreviewData[F]] |   ): OptionT[F, AttachmentPreviewData[F]] | ||||||
|  |  | ||||||
|  |   def findAttachment(attachId: Ident, shareId: Ident): OptionT[F, AttachmentData[F]] | ||||||
|  |  | ||||||
|   def searchSummary( |   def searchSummary( | ||||||
|       settings: OSimpleSearch.StatsSettings |       settings: OSimpleSearch.StatsSettings | ||||||
|   )(shareId: Ident, q: ItemQueryString): OptionT[F, StringSearchResult[SearchSummary]] |   )(shareId: Ident, q: ItemQueryString): OptionT[F, StringSearchResult[SearchSummary]] | ||||||
| @@ -232,24 +234,36 @@ object OShare { | |||||||
|       ): OptionT[F, AttachmentPreviewData[F]] = |       ): OptionT[F, AttachmentPreviewData[F]] = | ||||||
|         for { |         for { | ||||||
|           sq <- findShareQuery(shareId) |           sq <- findShareQuery(shareId) | ||||||
|           account = sq.asAccount |           _ <- checkAttachment(sq, attachId) | ||||||
|           checkQuery = Query( |           res <- OptionT(itemSearch.findAttachmentPreview(attachId, sq.cid)) | ||||||
|             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 |         } 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( |       def searchSummary( | ||||||
|           settings: OSimpleSearch.StatsSettings |           settings: OSimpleSearch.StatsSettings | ||||||
|       )( |       )( | ||||||
|   | |||||||
| @@ -7,9 +7,11 @@ | |||||||
| package docspell.restserver.conv | package docspell.restserver.conv | ||||||
|  |  | ||||||
| import java.time.{LocalDate, ZoneId} | import java.time.{LocalDate, ZoneId} | ||||||
|  |  | ||||||
| import cats.effect.{Async, Sync} | import cats.effect.{Async, Sync} | ||||||
| import cats.implicits._ | import cats.implicits._ | ||||||
| import fs2.Stream | import fs2.Stream | ||||||
|  |  | ||||||
| import docspell.backend.ops.OCollective.{InsightData, PassChangeResult} | import docspell.backend.ops.OCollective.{InsightData, PassChangeResult} | ||||||
| import docspell.backend.ops.OCustomFields.SetValueResult | import docspell.backend.ops.OCustomFields.SetValueResult | ||||||
| import docspell.backend.ops.OJob.JobCancelResult | 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.queries.{AttachmentLight => QAttachmentLight, IdRefCount} | ||||||
| import docspell.store.records._ | import docspell.store.records._ | ||||||
| import docspell.store.{AddResult, UpdateResult} | import docspell.store.{AddResult, UpdateResult} | ||||||
|  |  | ||||||
| import org.http4s.headers.`Content-Type` | import org.http4s.headers.`Content-Type` | ||||||
| import org.http4s.multipart.Multipart | import org.http4s.multipart.Multipart | ||||||
| import org.log4s.Logger | import org.log4s.Logger | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ 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.OItemSearch.{AttachmentData, AttachmentPreviewData} | ||||||
| import docspell.backend.ops._ | import docspell.backend.ops._ | ||||||
| import docspell.restapi.model.BasicResult | import docspell.restapi.model.BasicResult | ||||||
| import docspell.restserver.http4s.{QueryParam => QP} | import docspell.restserver.http4s.{QueryParam => QP} | ||||||
| @@ -27,6 +27,31 @@ import org.typelevel.ci.CIString | |||||||
| object BinaryUtil { | object BinaryUtil { | ||||||
|  |  | ||||||
|   def respond[F[_]: Async](dsl: Http4sDsl[F], req: Request[F])( |   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]] |       fileData: Option[AttachmentPreviewData[F]] | ||||||
|   ): F[Response[F]] = { |   ): F[Response[F]] = { | ||||||
|     import dsl._ |     import dsl._ | ||||||
| @@ -54,7 +79,7 @@ object BinaryUtil { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   def respondHead[F[_]: Async]( |   def respondPreviewHead[F[_]: Async]( | ||||||
|       dsl: Http4sDsl[F] |       dsl: Http4sDsl[F] | ||||||
|   )(fileData: Option[AttachmentPreviewData[F]]): F[Response[F]] = { |   )(fileData: Option[AttachmentPreviewData[F]]): F[Response[F]] = { | ||||||
|     import dsl._ |     import dsl._ | ||||||
|   | |||||||
| @@ -46,24 +46,13 @@ object AttachmentRoutes { | |||||||
|       case HEAD -> Root / Ident(id) => |       case HEAD -> Root / Ident(id) => | ||||||
|         for { |         for { | ||||||
|           fileData <- backend.itemSearch.findAttachment(id, user.account.collective) |           fileData <- backend.itemSearch.findAttachment(id, user.account.collective) | ||||||
|           resp <- |           resp <- BinaryUtil.respondHead(dsl)(fileData) | ||||||
|             fileData |  | ||||||
|               .map(data => withResponseHeaders(Ok())(data)) |  | ||||||
|               .getOrElse(NotFound(BasicResult(false, "Not found"))) |  | ||||||
|         } yield resp |         } yield resp | ||||||
|  |  | ||||||
|       case req @ GET -> Root / Ident(id) => |       case req @ GET -> Root / Ident(id) => | ||||||
|         for { |         for { | ||||||
|           fileData <- backend.itemSearch.findAttachment(id, user.account.collective) |           fileData <- backend.itemSearch.findAttachment(id, user.account.collective) | ||||||
|           inm = req.headers.get[`If-None-Match`].flatMap(_.tags) |           resp <- BinaryUtil.respond[F](dsl, req)(fileData) | ||||||
|           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"))) |  | ||||||
|         } yield resp |         } yield resp | ||||||
|  |  | ||||||
|       case HEAD -> Root / Ident(id) / "original" => |       case HEAD -> Root / Ident(id) / "original" => | ||||||
| @@ -118,14 +107,14 @@ object AttachmentRoutes { | |||||||
|         for { |         for { | ||||||
|           fileData <- |           fileData <- | ||||||
|             backend.itemSearch.findAttachmentPreview(id, user.account.collective) |             backend.itemSearch.findAttachmentPreview(id, user.account.collective) | ||||||
|           resp <- BinaryUtil.respond(dsl, req)(fileData) |           resp <- BinaryUtil.respondPreview(dsl, req)(fileData) | ||||||
|         } yield resp |         } yield resp | ||||||
|  |  | ||||||
|       case HEAD -> Root / Ident(id) / "preview" => |       case HEAD -> Root / Ident(id) / "preview" => | ||||||
|         for { |         for { | ||||||
|           fileData <- |           fileData <- | ||||||
|             backend.itemSearch.findAttachmentPreview(id, user.account.collective) |             backend.itemSearch.findAttachmentPreview(id, user.account.collective) | ||||||
|           resp <- BinaryUtil.respondHead(dsl)(fileData) |           resp <- BinaryUtil.respondPreviewHead(dsl)(fileData) | ||||||
|         } yield resp |         } yield resp | ||||||
|  |  | ||||||
|       case POST -> Root / Ident(id) / "preview" => |       case POST -> Root / Ident(id) / "preview" => | ||||||
|   | |||||||
| @@ -13,9 +13,11 @@ import docspell.backend.BackendApp | |||||||
| import docspell.backend.auth.ShareToken | import docspell.backend.auth.ShareToken | ||||||
| import docspell.common._ | import docspell.common._ | ||||||
| import docspell.restserver.http4s.BinaryUtil | 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.dsl.Http4sDsl | ||||||
|  | import org.http4s.headers._ | ||||||
|  |  | ||||||
| object ShareAttachmentRoutes { | object ShareAttachmentRoutes { | ||||||
|  |  | ||||||
| @@ -27,18 +29,35 @@ object ShareAttachmentRoutes { | |||||||
|     import dsl._ |     import dsl._ | ||||||
|  |  | ||||||
|     HttpRoutes.of { |     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" => |       case req @ GET -> Root / Ident(id) / "preview" => | ||||||
|         for { |         for { | ||||||
|           fileData <- |           fileData <- backend.share.findAttachmentPreview(id, token.id).value | ||||||
|             backend.share.findAttachmentPreview(id, token.id).value |           resp <- BinaryUtil.respondPreview(dsl, req)(fileData) | ||||||
|           resp <- BinaryUtil.respond(dsl, req)(fileData) |  | ||||||
|         } yield resp |         } yield resp | ||||||
|  |  | ||||||
|       case HEAD -> Root / Ident(id) / "preview" => |       case HEAD -> Root / Ident(id) / "preview" => | ||||||
|         for { |         for { | ||||||
|           fileData <- |           fileData <- backend.share.findAttachmentPreview(id, token.id).value | ||||||
|             backend.share.findAttachmentPreview(id, token.id).value |           resp <- BinaryUtil.respondPreviewHead(dsl)(fileData) | ||||||
|           resp <- BinaryUtil.respondHead(dsl)(fileData) |  | ||||||
|         } yield resp |         } yield resp | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,3 +1,9 @@ | |||||||
|  | /* | ||||||
|  |  * Copyright 2020 Eike K. & Contributors | ||||||
|  |  * | ||||||
|  |  * SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
|  |  */ | ||||||
|  |  | ||||||
| package docspell.store.queries | package docspell.store.queries | ||||||
|  |  | ||||||
| import docspell.common._ | import docspell.common._ | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user