Initial application stub

This commit is contained in:
Eike Kettner
2019-07-17 22:03:10 +02:00
commit 6154e6a387
54 changed files with 2447 additions and 0 deletions

View File

@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<withJansi>true</withJansi>
<encoder>
<pattern>[%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n</pattern>
</encoder>
</appender>
<logger name="docspell" level="debug" />
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -0,0 +1,3 @@
docspell.restserver {
}

View File

@ -0,0 +1,19 @@
package docspell.restserver
import docspell.store.JdbcConfig
case class Config(appName: String
, baseUrl: String
, bind: Config.Bind
, jdbc: JdbcConfig
)
object Config {
val default: Config =
Config("Docspell", "http://localhost:7880", Config.Bind("localhost", 7880), JdbcConfig("", "", ""))
case class Bind(address: String, port: Int)
}

View File

@ -0,0 +1,26 @@
package docspell.restserver
import cats.effect._
import org.http4s._
import org.http4s.HttpRoutes
import org.http4s.dsl.Http4sDsl
import org.http4s.circe.CirceEntityEncoder._
import docspell.restapi.model._
import docspell.restserver.BuildInfo
object InfoRoutes {
def apply[F[_]: Sync](cfg: Config): HttpRoutes[F] = {
val dsl = new Http4sDsl[F]{}
import dsl._
HttpRoutes.of[F] {
case GET -> (Root / "version") =>
Ok(VersionInfo(BuildInfo.version
, BuildInfo.builtAtMillis
, BuildInfo.builtAtString
, BuildInfo.gitHeadCommit.getOrElse("")
, BuildInfo.gitDescribedVersion.getOrElse("")))
}
}
}

View File

@ -0,0 +1,39 @@
package docspell.restserver
import cats.effect._
import cats.implicits._
import scala.concurrent.ExecutionContext
import java.util.concurrent.Executors
import java.nio.file.{Files, Paths}
import org.log4s._
object Main extends IOApp {
private[this] val logger = getLogger
val blockingEc: ExecutionContext = ExecutionContext.fromExecutor(Executors.newCachedThreadPool)
val blocker = Blocker.liftExecutionContext(blockingEc)
def run(args: List[String]) = {
args match {
case file :: Nil =>
val path = Paths.get(file).toAbsolutePath.normalize
logger.info(s"Using given config file: $path")
System.setProperty("config.file", file)
case _ =>
Option(System.getProperty("config.file")) match {
case Some(f) if f.nonEmpty =>
val path = Paths.get(f).toAbsolutePath.normalize
if (!Files.exists(path)) {
logger.info(s"Not using config file '$f' because it doesn't exist")
System.clearProperty("config.file")
} else {
logger.info(s"Using config file from system properties: $f")
}
case _ =>
}
}
val cfg = Config.default
RestServer.stream[IO](cfg, blocker).compile.drain.as(ExitCode.Success)
}
}

View File

@ -0,0 +1,6 @@
package docspell.restserver
trait RestApp[F[_]] {
def init: F[Unit]
}

View File

@ -0,0 +1,16 @@
package docspell.restserver
import cats.effect._
final class RestAppImpl[F[_]: Sync](cfg: Config) extends RestApp[F] {
def init: F[Unit] =
Sync[F].pure(())
}
object RestAppImpl {
def create[F[_]: Sync](cfg: Config): Resource[F, RestApp[F]] =
Resource.liftF(Sync[F].pure(new RestAppImpl(cfg)))
}

View File

@ -0,0 +1,42 @@
package docspell.restserver
import cats.effect._
import org.http4s.server.blaze.BlazeServerBuilder
import org.http4s.implicits._
import fs2.Stream
import org.http4s.server.middleware.Logger
import org.http4s.server.Router
import docspell.restserver.webapp._
object RestServer {
def stream[F[_]: ConcurrentEffect](cfg: Config, blocker: Blocker)
(implicit T: Timer[F], CS: ContextShift[F]): Stream[F, Nothing] = {
val app = for {
restApp <- RestAppImpl.create[F](cfg)
_ <- Resource.liftF(restApp.init)
httpApp = Router(
"/api/info" -> InfoRoutes(cfg),
"/app/assets" -> WebjarRoutes.appRoutes[F](blocker, cfg),
"/app" -> TemplateRoutes[F](blocker, cfg)
).orNotFound
// With Middlewares in place
finalHttpApp = Logger.httpApp(false, false)(httpApp)
} yield finalHttpApp
Stream.resource(app).flatMap(httpApp =>
BlazeServerBuilder[F]
.bindHttp(cfg.bind.port, cfg.bind.address)
.withHttpApp(httpApp)
.serve
)
}.drain
}

View File

@ -0,0 +1,139 @@
package docspell.restserver.webapp
import fs2._
import cats.effect._
import cats.implicits._
import org.http4s._
import org.http4s.headers._
import org.http4s.HttpRoutes
import org.http4s.dsl.Http4sDsl
import org.slf4j._
import _root_.io.circe._
import _root_.io.circe.generic.semiauto._
import _root_.io.circe.syntax._
import yamusca.imports._
import yamusca.implicits._
import java.net.URL
import java.util.concurrent.atomic.AtomicReference
import docspell.restserver.{BuildInfo, Config}
object TemplateRoutes {
private[this] val logger = LoggerFactory.getLogger(getClass)
val `text/html` = new MediaType("text", "html")
def apply[F[_]: Effect](blocker: Blocker, cfg: Config)(implicit C: ContextShift[F]): HttpRoutes[F] = {
val indexTemplate = memo(loadResource("/index.html").flatMap(loadTemplate(_, blocker)))
val docTemplate = memo(loadResource("/doc.html").flatMap(loadTemplate(_, blocker)))
val dsl = new Http4sDsl[F]{}
import dsl._
HttpRoutes.of[F] {
case GET -> Root / "index.html" =>
for {
templ <- indexTemplate
resp <- Ok(IndexData(cfg).render(templ), `Content-Type`(`text/html`))
} yield resp
case GET -> Root / "doc" =>
for {
templ <- docTemplate
resp <- Ok(DocData(cfg).render(templ), `Content-Type`(`text/html`))
} yield resp
}
}
def loadResource[F[_]: Sync](name: String): F[URL] = {
Option(getClass.getResource(name)) match {
case None =>
Sync[F].raiseError(new Exception("Unknown resource: "+ name))
case Some(r) =>
r.pure[F]
}
}
def loadUrl[F[_]: Sync](url: URL, blocker: Blocker)(implicit C: ContextShift[F]): F[String] =
Stream.bracket(Sync[F].delay(url.openStream))(in => Sync[F].delay(in.close)).
flatMap(in => io.readInputStream(in.pure[F], 64 * 1024, blocker, false)).
through(text.utf8Decode).
compile.fold("")(_ + _)
def parseTemplate[F[_]: Sync](str: String): F[Template] =
Sync[F].delay {
mustache.parse(str) match {
case Right(t) => t
case Left((_, err)) => sys.error(err)
}
}
def loadTemplate[F[_]: Sync](url: URL, blocker: Blocker)(implicit C: ContextShift[F]): F[Template] = {
loadUrl[F](url, blocker).flatMap(s => parseTemplate(s)).
map(t => {
logger.info(s"Compiled template $url")
t
})
}
case class DocData(swaggerRoot: String, openapiSpec: String)
object DocData {
def apply(cfg: Config): DocData =
DocData("/app/assets" + Webjars.swaggerui, s"/app/assets/${BuildInfo.name}/${BuildInfo.version}/openapi.yml")
implicit def yamuscaValueConverter: ValueConverter[DocData] =
ValueConverter.deriveConverter[DocData]
}
case class Flags(appName: String, baseUrl: String)
object Flags {
def apply(cfg: Config): Flags =
Flags(cfg.appName, cfg.baseUrl)
implicit val jsonEncoder: Encoder[Flags] =
deriveEncoder[Flags]
implicit def yamuscaValueConverter: ValueConverter[Flags] =
ValueConverter.deriveConverter[Flags]
}
case class IndexData(flags: Flags
, cssUrls: Seq[String]
, jsUrls: Seq[String]
, appExtraJs: String
, flagsJson: String)
object IndexData {
def apply(cfg: Config): IndexData =
IndexData(Flags(cfg)
, Seq(
"/app/assets" + Webjars.semanticui + "/semantic.min.css",
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.css"
)
, Seq(
"/app/assets" + Webjars.jquery + "/jquery.min.js",
"/app/assets" + Webjars.semanticui + "/semantic.min.js",
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-app.js"
)
,
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.js"
, Flags(cfg).asJson.spaces2 )
implicit def yamuscaValueConverter: ValueConverter[IndexData] =
ValueConverter.deriveConverter[IndexData]
}
private def memo[F[_]: Sync, A](fa: => F[A]): F[A] = {
val ref = new AtomicReference[A]()
Sync[F].suspend {
Option(ref.get) match {
case Some(a) => a.pure[F]
case None =>
fa.map(a => {
ref.set(a)
a
})
}
}
}
}

View File

@ -0,0 +1,28 @@
package docspell.restserver.webapp
import cats.effect._
import org.http4s._
import org.http4s.HttpRoutes
import org.http4s.server.staticcontent.webjarService
import org.http4s.server.staticcontent.NoopCacheStrategy
import org.http4s.server.staticcontent.WebjarService.{WebjarAsset, Config => WebjarConfig}
import docspell.restserver.Config
object WebjarRoutes {
def appRoutes[F[_]: Effect](blocker: Blocker, cfg: Config)(implicit C: ContextShift[F]): HttpRoutes[F] = {
webjarService(
WebjarConfig(
filter = assetFilter,
blocker = blocker,
cacheStrategy = NoopCacheStrategy[F]
)
)
}
def assetFilter(asset: WebjarAsset): Boolean =
List(".js", ".css", ".html", ".jpg", ".png", ".eot", ".woff", ".woff2", ".svg", ".otf", ".ttf", ".yml").
exists(e => asset.asset.endsWith(e))
}

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Swagger UI</title>
<link rel="stylesheet" type="text/css" href="{{swaggerRoot}}/swagger-ui.css" >
<link rel="icon" type="image/png" href="{{swaggerRoot}}/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="{{swaggerRoot}}/favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="{{swaggerRoot}}/swagger-ui-bundle.js"> </script>
<script src="{{swaggerRoot}}/swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "{{openapiSpec}}",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "BaseLayout",
displayRequestDuration: true
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>

View File

@ -0,0 +1,31 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="mobile-web-app-capable" content="yes">
<title>{{ flags.appName }}</title>
{{# cssUrls }}
<link rel="stylesheet" href="{{.}}"/>
{{/ cssUrls }}
{{# jsUrls }}
<script type="application/javascript" src="{{.}}"></script>
{{/ jsUrls}}
</head>
<body>
<div id="docspell-app">
</div>
<script type="application/javascript">
var storedAccount = localStorage.getItem('account');
var account = storedAccount ? JSON.parse(storedAccount) : null;
var elmFlags = {
"account": account,
"config": {{{flagsJson}}}
};
</script>
<script type="application/javascript" src="{{appExtraJs}}"></script>
</body>
</html>