diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala b/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala index 44fe2e71..ff312503 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OItemSearch.scala @@ -36,6 +36,11 @@ trait OItemSearch[F[_]] { collective: Ident ): F[Option[AttachmentArchiveData[F]]] + def findAttachmentPreview( + id: Ident, + collective: Ident + ): F[Option[AttachmentPreviewData[F]]] + def findAttachmentMeta(id: Ident, collective: Ident): F[Option[RAttachmentMeta]] def findByFileCollective(checksum: String, collective: Ident): F[Vector[RItem]] @@ -82,6 +87,15 @@ object OItemSearch { val fileId = rs.fileId } + case class AttachmentPreviewData[F[_]]( + rs: RAttachmentPreview, + meta: FileMeta, + data: Stream[F, Byte] + ) extends BinaryData[F] { + val name = rs.name + val fileId = rs.fileId + } + case class AttachmentArchiveData[F[_]]( rs: RAttachmentArchive, meta: FileMeta, @@ -154,6 +168,26 @@ object OItemSearch { (None: Option[AttachmentSourceData[F]]).pure[F] }) + def findAttachmentPreview( + id: Ident, + collective: Ident + ): F[Option[AttachmentPreviewData[F]]] = + store + .transact(RAttachmentPreview.findByIdAndCollective(id, collective)) + .flatMap({ + case Some(ra) => + makeBinaryData(ra.fileId) { m => + AttachmentPreviewData[F]( + ra, + m, + store.bitpeace.fetchData2(RangeDef.all)(Stream.emit(m)) + ) + } + + case None => + (None: Option[AttachmentPreviewData[F]]).pure[F] + }) + def findAttachmentArchive( id: Ident, collective: Ident diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index cc929c4b..4aa09894 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -2446,6 +2446,45 @@ paths: schema: type: string format: binary + /sec/attachment/{id}/preview: + head: + tags: [ Attachment ] + summary: Get a preview image of an attachment file. + description: | + Checks if an image file showing a preview of the attachment is + available. If not available, a 404 is returned. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + responses: + 200: + description: Ok + 404: + description: NotFound + get: + tags: [ Attachment ] + summary: Get a preview image of an attachment file. + description: | + Gets a image file showing a preview of the attachment. Usually + it is a small image of the first page of the document.If not + available, a 404 is returned. However, if the query parameter + `withFallback` is `true`, a fallback preview image is + returned. You can also use the `HEAD` method to check for + existence. + security: + - authTokenHeader: [] + parameters: + - $ref: "#/components/parameters/id" + - $ref: "#/components/parameters/withFallback" + responses: + 200: + description: Ok + content: + application/octet-stream: + schema: + type: string + format: binary /sec/attachment/{id}/meta: get: tags: [ Attachment ] @@ -4822,3 +4861,10 @@ components: One of the available contact kinds. schema: type: string + withFallback: + name: withFallback + in: query + description: Whether to provide a fallback or not. + required: false + schema: + type: boolean 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 91c574e7..bc500bdd 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/AttachmentRoutes.scala @@ -117,6 +117,31 @@ object AttachmentRoutes { .getOrElse(NotFound(BasicResult(false, "Not found"))) } yield resp + 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 = 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 + + case HEAD -> Root / Ident(id) / "preview" => + for { + fileData <- + backend.itemSearch.findAttachmentPreview(id, user.account.collective) + resp <- + fileData + .map(data => withResponseHeaders(Ok())(data)) + .getOrElse(NotFound(BasicResult(false, "Not found"))) + } yield resp + case GET -> Root / Ident(id) / "view" => // this route exists to provide a stable url // it redirects currently to viewerjs