Add attachment preview url based on ViewerJS

The viewerJS library can display PDF files easily using pdfjs. Another
attachment route redirects to the viewerjs application to display the
current attachment.

The attachment responses have been improved in that now the response
headers are added to all responses. Additional a HEAD route has been
added to support the viewerJS application.
This commit is contained in:
Eike Kettner 2020-02-08 17:51:47 +01:00
parent e1826f39ac
commit 8908ad2561
3 changed files with 92 additions and 15 deletions

View File

@ -1168,6 +1168,32 @@ paths:
$ref: "#/components/schemas/ItemProposals"
/sec/attachment/{id}:
head:
tags: [ Attachment ]
summary: Get an attachment file.
description: |
Get the binary file belonging to the attachment with the given id.
security:
- authTokenHeader: []
parameters:
- $ref: "#/components/parameters/id"
responses:
200:
description: Ok
headers:
Content-Type:
schema:
type: string
Content-Length:
schema:
type: integer
format: int64
ETag:
schema:
type: string
Content-Disposition:
schema:
type: string
get:
tags: [ Attachment ]
summary: Get an attachment file.
@ -1203,6 +1229,27 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/AttachmentMeta"
/sec/attachment/{id}/view:
get:
tags: [ Attachment ]
summary: A preview of the attachment
description: |
This provides a preview of the attachment. It currently uses a
third-party javascript library (viewerjs) to display the
preview. This works by redirecting to the viewerjs url with
the attachment url as parameter. Note that the resulting url
that is redirected to is not stable. It may change from
version to version. This route, however, is meant to provide a
stable url for the preview.
security:
- authTokenHeader: []
parameters:
- $ref: "#/components/parameters/id"
responses:
303:
description: See Other
200:
description: Ok
/sec/queue/state:
get:
tags: [ Job Queue ]

View File

@ -3,17 +3,18 @@ package docspell.restserver.routes
import cats.data.NonEmptyList
import cats.effect._
import cats.implicits._
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.headers._
import org.http4s.headers.ETag.EntityTag
import org.http4s.circe.CirceEntityEncoder._
import docspell.backend.BackendApp
import docspell.backend.auth.AuthToken
import docspell.backend.ops.OItem
import docspell.common.Ident
import org.http4s.{Header, HttpRoutes, MediaType, Response}
import org.http4s.dsl.Http4sDsl
import org.http4s.headers._
import org.http4s.circe.CirceEntityEncoder._
import docspell.restapi.model._
import docspell.restserver.conv.Conversions
import org.http4s.headers.ETag.EntityTag
import docspell.restserver.webapp.Webjars
object AttachmentRoutes {
@ -21,28 +22,52 @@ object AttachmentRoutes {
val dsl = new Http4sDsl[F] {}
import dsl._
def makeByteResp(data: OItem.AttachmentData[F]): F[Response[F]] = {
def withResponseHeaders(resp: F[Response[F]])(data: OItem.AttachmentData[F]): F[Response[F]] = {
val mt = MediaType.unsafeParse(data.meta.mimetype.asString)
val ctype = `Content-Type`(mt)
val cntLen: Header = `Content-Length`.unsafeFromLong(data.meta.length)
val eTag: Header = ETag(data.meta.checksum)
val disp: Header =
`Content-Disposition`("inline", Map("filename" -> data.ra.name.getOrElse("")))
Ok(data.data.take(data.meta.length)).map(r =>
r.withContentType(`Content-Type`(mt)).withHeaders(cntLen, eTag, disp)
resp.map(r =>
if (r.status == NotModified) r.withHeaders(ctype, eTag, disp)
else r.withHeaders(ctype, cntLen, eTag, disp)
)
}
def makeByteResp(data: OItem.AttachmentData[F]): F[Response[F]] =
withResponseHeaders(Ok(data.data.take(data.meta.length)))(data)
HttpRoutes.of {
case HEAD -> Root / Ident(id) =>
for {
fileData <- backend.item.findAttachment(id, user.account.collective)
resp <- fileData
.map(data => withResponseHeaders(Ok())(data))
.getOrElse(NotFound(BasicResult(false, "Not found")))
} yield resp
case req @ GET -> Root / Ident(id) =>
for {
fileData <- backend.item.findAttachment(id, user.account.collective)
inm = req.headers.get(`If-None-Match`).flatMap(_.tags)
matches = matchETag(fileData, inm)
resp <- if (matches) NotModified()
else
fileData.map(makeByteResp).getOrElse(NotFound(BasicResult(false, "Not found")))
resp <- fileData
.map({ data =>
if (matches) withResponseHeaders(NotModified())(data)
else makeByteResp(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
val attachUrl = s"/api/v1/sec/attachment/${id.id}"
val path = s"/app/assets${Webjars.viewerjs}/ViewerJS/index.html#$attachUrl"
SeeOther(Location(Uri(path = path)))
case GET -> Root / Ident(id) / "meta" =>
for {
rm <- backend.item.findAttachmentMeta(id, user.account.collective)

View File

@ -26,6 +26,10 @@ object Dependencies {
val StanfordNlpVersion = "3.9.2"
val TikaVersion = "1.23"
val YamuscaVersion = "0.6.1"
val SwaggerUIVersion = "3.24.3"
val SemanticUIVersion = "2.4.1"
val JQueryVersion = "3.4.1"
val ViewerJSVersion = "0.5.8"
val emil = Seq(
"com.github.eikek" %% "emil-common" % EmilVersion,
@ -149,9 +153,10 @@ object Dependencies {
val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % BetterMonadicForVersion
val webjars = Seq(
"swagger-ui" -> "3.24.3",
"Semantic-UI" -> "2.4.1",
"jquery" -> "3.4.1"
).map({case (a, v) => "org.webjars" % a % v })
"org.webjars" % "swagger-ui" % SwaggerUIVersion,
"org.webjars" % "Semantic-UI"% SemanticUIVersion,
"org.webjars" % "jquery" % JQueryVersion,
"org.webjars" % "viewerjs" % ViewerJSVersion
)
}