Provide fallback image for previews

This commit is contained in:
Eike Kettner 2020-11-09 08:57:43 +01:00
parent d4bbb936b6
commit 8c8788bc69
7 changed files with 178 additions and 15 deletions

View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="no-preview.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="638.19656"
inkscape:cy="138.48596"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1896"
inkscape:window-height="2101"
inkscape:window-x="3844"
inkscape:window-y="39"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.50112426;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="rect4518"
width="209.3988"
height="297.08929"
x="0.37797618"
y="0.28869045" />
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:4.23650599px;line-height:2;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:center;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:middle;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26478162"
x="101.91397"
y="163.31726"
id="text4554"
transform="scale(0.99565662,1.0043623)"><tspan
sodipodi:role="line"
id="tspan4552"
x="101.91397"
y="163.31726"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:22.59469986px;line-height:2;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold';text-align:center;text-anchor:middle;stroke-width:0.26478162">Preview</tspan><tspan
sodipodi:role="line"
x="101.91397"
y="208.50665"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:22.59469986px;line-height:2;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold';text-align:center;text-anchor:middle;stroke-width:0.26478162"
id="tspan4556">not</tspan><tspan
sodipodi:role="line"
x="101.91397"
y="253.69606"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:22.59469986px;line-height:2;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold';text-align:center;text-anchor:middle;stroke-width:0.26478162"
id="tspan4558">available</tspan></text>
<path
style="opacity:1;fill:#d7e3f4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.64033973;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="path4581"
sodipodi:type="arc"
sodipodi:cx="103.00598"
sodipodi:cy="78.360054"
sodipodi:rx="34.460411"
sodipodi:ry="34.761723"
sodipodi:start="0.34567468"
sodipodi:end="0.34365009"
sodipodi:open="true"
d="M 135.42796,90.138421 A 34.460411,34.761723 0 0 1 91.34612,111.07148 34.460411,34.761723 0 0 1 70.572202,66.6148 34.460411,34.761723 0 0 1 114.63301,45.636742 a 34.460411,34.761723 0 0 1 20.81852,44.43544" />
<rect
style="opacity:1;fill:#000055;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.78938425;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="rect4583"
width="35.846756"
height="4.1953807"
x="84.785538"
y="90.746422" />
<path
style="opacity:1;fill:#000055;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.78938425;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="path4585"
sodipodi:type="arc"
sodipodi:cx="117.95863"
sodipodi:cy="66.872711"
sodipodi:rx="5.8424263"
sodipodi:ry="5.8935103"
sodipodi:start="0.34567468"
sodipodi:end="0.34365009"
sodipodi:open="true"
d="m 123.45546,68.869618 a 5.8424263,5.8935103 0 0 1 -7.47364,3.548995 5.8424263,5.8935103 0 0 1 -3.52202,-7.537195 5.8424263,5.8935103 0 0 1 7.47008,-3.556625 5.8424263,5.8935103 0 0 1 3.52958,7.533595" />
<path
style="opacity:1;fill:#000055;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.78938425;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:stroke fill markers"
id="path4585-8"
sodipodi:type="arc"
sodipodi:cx="87.558212"
sodipodi:cy="67.172394"
sodipodi:rx="5.8424263"
sodipodi:ry="5.8935103"
sodipodi:start="0.34567468"
sodipodi:end="0.34365009"
sodipodi:open="true"
d="m 93.055042,69.169301 a 5.8424263,5.8935103 0 0 1 -7.473645,3.548995 5.8424263,5.8935103 0 0 1 -3.522015,-7.537195 5.8424263,5.8935103 0 0 1 7.47008,-3.556625 5.8424263,5.8935103 0 0 1 3.529577,7.533595" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -33,7 +33,7 @@ object RestServer {
"/api/info" -> routes.InfoRoutes(),
"/api/v1/open/" -> openRoutes(cfg, restApp),
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
securedRoutes(cfg, restApp, token)
securedRoutes(cfg, pools, restApp, token)
},
"/api/doc" -> templates.doc,
"/app/assets" -> WebjarRoutes.appRoutes[F](pools.blocker),
@ -57,8 +57,9 @@ object RestServer {
)
}.drain
def securedRoutes[F[_]: Effect](
def securedRoutes[F[_]: Effect: ContextShift](
cfg: Config,
pools: Pools,
restApp: RestApp[F],
token: AuthToken
): HttpRoutes[F] =
@ -72,9 +73,9 @@ object RestServer {
"user" -> UserRoutes(restApp.backend, token),
"collective" -> CollectiveRoutes(restApp.backend, token),
"queue" -> JobQueueRoutes(restApp.backend, token),
"item" -> ItemRoutes(cfg, restApp.backend, token),
"item" -> ItemRoutes(cfg, pools.blocker, restApp.backend, token),
"items" -> ItemMultiRoutes(restApp.backend, token),
"attachment" -> AttachmentRoutes(restApp.backend, token),
"attachment" -> AttachmentRoutes(pools.blocker, restApp.backend, token),
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token),
"checkfile" -> CheckFileRoutes.secured(restApp.backend, token),
"email/send" -> MailSendRoutes(restApp.backend, token),

View File

@ -1,6 +1,7 @@
package docspell.restserver.http4s
import cats.data.NonEmptyList
import cats.data.OptionT
import cats.effect._
import cats.implicits._
@ -51,4 +52,16 @@ object BinaryUtil {
false
}
def noPreview[F[_]: Sync: ContextShift](
blocker: Blocker,
req: Option[Request[F]]
): OptionT[F, Response[F]] =
StaticFile.fromResource(
name = "/docspell/restserver/no-preview.svg",
blocker = blocker,
req = req,
preferGzipped = true,
classloader = getClass.getClassLoader().some
)
}

View File

@ -29,4 +29,6 @@ object QueryParam {
object ContactKindOpt extends OptionalQueryParamDecoderMatcher[ContactKind]("kind")
object QueryOpt extends OptionalQueryParamDecoderMatcher[QueryString]("q")
object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback")
}

View File

@ -11,6 +11,7 @@ import docspell.common.MakePreviewArgs
import docspell.restapi.model._
import docspell.restserver.conv.Conversions
import docspell.restserver.http4s.BinaryUtil
import docspell.restserver.http4s.{QueryParam => QP}
import docspell.restserver.webapp.Webjars
import org.http4s._
@ -21,7 +22,11 @@ import org.http4s.headers._
object AttachmentRoutes {
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
def apply[F[_]: Effect: ContextShift](
blocker: Blocker,
backend: BackendApp[F],
user: AuthToken
): HttpRoutes[F] = {
val dsl = new Http4sDsl[F] {}
import dsl._
@ -105,19 +110,25 @@ object AttachmentRoutes {
.getOrElse(NotFound(BasicResult(false, "Not found")))
} yield resp
case req @ GET -> Root / Ident(id) / "preview" =>
case req @ GET -> Root / Ident(id) / "preview" :? QP.WithFallback(flag) =>
def notFound =
NotFound(BasicResult(false, "Not found"))
for {
fileData <-
backend.itemSearch.findAttachmentPreview(id, user.account.collective)
inm = req.headers.get(`If-None-Match`).flatMap(_.tags)
matches = BinaryUtil.matchETag(fileData.map(_.meta), inm)
inm = req.headers.get(`If-None-Match`).flatMap(_.tags)
matches = BinaryUtil.matchETag(fileData.map(_.meta), inm)
fallback = flag.getOrElse(false)
resp <-
fileData
.map { data =>
if (matches) withResponseHeaders(NotModified())(data)
else makeByteResp(data)
}
.getOrElse(NotFound(BasicResult(false, "Not found")))
.getOrElse(
if (fallback) BinaryUtil.noPreview(blocker, req.some).getOrElseF(notFound)
else notFound
)
} yield resp
case HEAD -> Root / Ident(id) / "preview" =>

View File

@ -15,6 +15,7 @@ import docspell.restserver.Config
import docspell.restserver.conv.Conversions
import docspell.restserver.http4s.BinaryUtil
import docspell.restserver.http4s.Responses
import docspell.restserver.http4s.{QueryParam => QP}
import org.http4s.HttpRoutes
import org.http4s.circe.CirceEntityDecoder._
@ -26,8 +27,9 @@ import org.log4s._
object ItemRoutes {
private[this] val logger = getLogger
def apply[F[_]: Effect](
def apply[F[_]: Effect: ContextShift](
cfg: Config,
blocker: Blocker,
backend: BackendApp[F],
user: AuthToken
): HttpRoutes[F] = {
@ -318,18 +320,24 @@ object ItemRoutes {
resp <- Ok(Conversions.basicResult(res, "Attachment moved."))
} yield resp
case req @ GET -> Root / Ident(id) / "preview" =>
case req @ GET -> Root / Ident(id) / "preview" :? QP.WithFallback(flag) =>
def notFound =
NotFound(BasicResult(false, "Not found"))
for {
preview <- backend.itemSearch.findItemPreview(id, user.account.collective)
inm = req.headers.get(`If-None-Match`).flatMap(_.tags)
matches = BinaryUtil.matchETag(preview.map(_.meta), inm)
inm = req.headers.get(`If-None-Match`).flatMap(_.tags)
matches = BinaryUtil.matchETag(preview.map(_.meta), inm)
fallback = flag.getOrElse(false)
resp <-
preview
.map { data =>
if (matches) BinaryUtil.withResponseHeaders(dsl, NotModified())(data)
else BinaryUtil.makeByteResp(dsl)(data).map(Responses.noCache)
}
.getOrElse(NotFound(BasicResult(false, "Not found")))
.getOrElse(
if (fallback) BinaryUtil.noPreview(blocker, req.some).getOrElseF(notFound)
else notFound
)
} yield resp
case HEAD -> Root / Ident(id) / "preview" =>

View File

@ -1505,7 +1505,7 @@ deleteAllItems flags ids receive =
itemPreviewURL : String -> String
itemPreviewURL itemId =
"/api/v1/sec/item/" ++ itemId ++ "/preview"
"/api/v1/sec/item/" ++ itemId ++ "/preview?withFallback=true"
fileURL : String -> String