mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-10-30 21:40:12 +00:00 
			
		
		
		
	Provide fallback image for previews
This commit is contained in:
		| @@ -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 | 
| @@ -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), | ||||
|   | ||||
| @@ -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 | ||||
|     ) | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -29,4 +29,6 @@ object QueryParam { | ||||
|   object ContactKindOpt extends OptionalQueryParamDecoderMatcher[ContactKind]("kind") | ||||
|  | ||||
|   object QueryOpt extends OptionalQueryParamDecoderMatcher[QueryString]("q") | ||||
|  | ||||
|   object WithFallback extends OptionalQueryParamDecoderMatcher[Boolean]("withFallback") | ||||
| } | ||||
|   | ||||
| @@ -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" => | ||||
|   | ||||
| @@ -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" => | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user