Merge pull request #1135 from eikek/improvement/1121-config

Improvement/1121 config
This commit is contained in:
mergify[bot] 2021-10-24 22:36:11 +00:00 committed by GitHub
commit 9af61cb4a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 497 additions and 128 deletions

1
.gitignore vendored
View File

@ -14,5 +14,6 @@ _site/
/website/site/templates/shortcodes/server.conf /website/site/templates/shortcodes/server.conf
/website/site/templates/shortcodes/sample-exim.conf /website/site/templates/shortcodes/sample-exim.conf
/website/site/templates/shortcodes/joex.conf /website/site/templates/shortcodes/joex.conf
/website/site/templates/shortcodes/config.env.txt
/docker/docs /docker/docs
/docker/dev-log /docker/dev-log

View File

@ -2,6 +2,7 @@ version = "3.0.6"
preset = default preset = default
align.preset = some align.preset = some
runner.dialect = scala213
maxColumn = 90 maxColumn = 90

View File

@ -293,10 +293,23 @@ val common = project
Dependencies.circe ++ Dependencies.circe ++
Dependencies.loggingApi ++ Dependencies.loggingApi ++
Dependencies.calevCore ++ Dependencies.calevCore ++
Dependencies.calevCirce ++ Dependencies.calevCirce
Dependencies.pureconfig.map(_ % "optional")
) )
val config = project
.in(file("modules/config"))
.disablePlugins(RevolverPlugin)
.settings(sharedSettings)
.settings(testSettingsMUnit)
.settings(
name := "docspell-config",
addCompilerPlugin(Dependencies.kindProjectorPlugin),
libraryDependencies ++=
Dependencies.fs2 ++
Dependencies.pureconfig
)
.dependsOn(common)
// Some example files for testing // Some example files for testing
// https://file-examples.com/index.php/sample-documents-download/sample-doc-download/ // https://file-examples.com/index.php/sample-documents-download/sample-doc-download/
val files = project val files = project
@ -603,7 +616,17 @@ val joex = project
), ),
Revolver.enableDebugging(port = 5051, suspend = false) Revolver.enableDebugging(port = 5051, suspend = false)
) )
.dependsOn(store, backend, extract, convert, analysis, joexapi, restapi, ftssolr) .dependsOn(
config,
store,
backend,
extract,
convert,
analysis,
joexapi,
restapi,
ftssolr
)
val restserver = project val restserver = project
.in(file("modules/restserver")) .in(file("modules/restserver"))
@ -666,7 +689,7 @@ val restserver = project
} }
} }
) )
.dependsOn(restapi, joexapi, backend, webapp, ftssolr, oidc) .dependsOn(config, restapi, joexapi, backend, webapp, ftssolr, oidc)
// --- Website Documentation // --- Website Documentation
@ -686,7 +709,6 @@ val website = project
val templateOut = baseDirectory.value / "site" / "templates" / "shortcodes" val templateOut = baseDirectory.value / "site" / "templates" / "shortcodes"
val staticOut = baseDirectory.value / "site" / "static" / "openapi" val staticOut = baseDirectory.value / "site" / "static" / "openapi"
IO.createDirectories(Seq(templateOut, staticOut)) IO.createDirectories(Seq(templateOut, staticOut))
val logger = streams.value.log
val files = Seq( val files = Seq(
(restserver / Compile / resourceDirectory).value / "reference.conf" -> templateOut / "server.conf", (restserver / Compile / resourceDirectory).value / "reference.conf" -> templateOut / "server.conf",
@ -698,6 +720,17 @@ val website = project
IO.copy(files) IO.copy(files)
files.map(_._2) files.map(_._2)
}.taskValue, }.taskValue,
Compile / resourceGenerators += Def.task {
val templateOut =
baseDirectory.value / "site" / "templates" / "shortcodes" / "config.env.txt"
val files = List(
(restserver / Compile / resourceDirectory).value / "reference.conf",
(joex / Compile / resourceDirectory).value / "reference.conf"
)
val cfg = EnvConfig.makeConfig(files)
EnvConfig.serializeTo(cfg, templateOut)
Seq(templateOut)
}.taskValue,
Compile / resourceGenerators += Def.task { Compile / resourceGenerators += Def.task {
val changelog = (LocalRootProject / baseDirectory).value / "Changelog.md" val changelog = (LocalRootProject / baseDirectory).value / "Changelog.md"
val targetDir = baseDirectory.value / "site" / "content" / "docs" / "changelog" val targetDir = baseDirectory.value / "site" / "content" / "docs" / "changelog"
@ -731,6 +764,7 @@ val root = project
) )
.aggregate( .aggregate(
common, common,
config,
extract, extract,
convert, convert,
analysis, analysis,

View File

@ -0,0 +1,108 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.config
import scala.reflect.ClassTag
import cats.data.OptionT
import cats.effect._
import cats.implicits._
import fs2.io.file.{Files, Path}
import docspell.common.Logger
import pureconfig.{ConfigReader, ConfigSource}
object ConfigFactory {
/** Reads the configuration trying the following in order:
* 1. if 'args' contains at least one element, the first is interpreted as a config
* file
* 1. otherwise check the system property 'config.file' for an existing file and use
* it if it does exist; ignore if it doesn't exist
* 1. if no file is found, read the config from environment variables falling back to
* the default config
*/
def default[F[_]: Async, C: ClassTag: ConfigReader](logger: Logger[F], atPath: String)(
args: List[String]
): F[C] =
findFileFromArgs(args).flatMap {
case Some(file) =>
logger.info(s"Using config file: $file") *>
readFile[F, C](file, atPath)
case None =>
checkSystemProperty.value.flatMap {
case Some(file) =>
logger.info(s"Using config file from system property: $file") *>
readConfig(atPath)
case None =>
logger.info("Using config from environment variables!") *>
readEnv(atPath)
}
}
/** Reads the configuration from the given file. */
private def readFile[F[_]: Sync, C: ClassTag: ConfigReader](
file: Path,
at: String
): F[C] =
Sync[F].delay {
System.setProperty(
"config.file",
file.toNioPath.toAbsolutePath.normalize.toString
)
ConfigSource.default.at(at).loadOrThrow[C]
}
/** Reads the config as specified in typesafe's config library; usually loading the file
* given as system property 'config.file'.
*/
private def readConfig[F[_]: Sync, C: ClassTag: ConfigReader](
at: String
): F[C] =
Sync[F].delay(ConfigSource.default.at(at).loadOrThrow[C])
/** Reads the configuration from environment variables. */
private def readEnv[F[_]: Sync, C: ClassTag: ConfigReader](at: String): F[C] =
Sync[F].delay(ConfigSource.fromConfig(EnvConfig.get).at(at).loadOrThrow[C])
/** Uses the first argument as a path to the config file. If it is specified but the
* file doesn't exist, an exception is thrown.
*/
private def findFileFromArgs[F[_]: Async](args: List[String]): F[Option[Path]] =
args.headOption
.map(Path.apply)
.traverse(p =>
Files[F].exists(p).flatMap {
case true => p.pure[F]
case false => Async[F].raiseError(new Exception(s"File not found: $p"))
}
)
/** If the system property 'config.file' is set, it is checked whether the file exists.
* If it doesn't exist, the property is removed to not raise any exception. In contrast
* to giving the file as argument, it is not an error to specify a non-existing file
* via a system property.
*/
private def checkSystemProperty[F[_]: Async]: OptionT[F, Path] =
for {
cf <- OptionT(
Sync[F].delay(
Option(System.getProperty("config.file")).map(_.trim).filter(_.nonEmpty)
)
)
cp = Path(cf)
exists <- OptionT.liftF(Files[F].exists(cp))
file <-
if (exists) OptionT.pure[F](cp)
else
OptionT
.liftF(Sync[F].delay(System.clearProperty("config.file")))
.flatMap(_ => OptionT.none[F, Path])
} yield file
}

View File

@ -0,0 +1,78 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.config
import java.util.Properties
import scala.collection.{MapView, mutable}
import scala.jdk.CollectionConverters._
import com.typesafe.config.{Config, ConfigFactory}
/** Creates a config from environment variables.
*
* The env variables are expected to be in same form as the config keys with the
* following mangling: a dot is replaced by an underscore character, because this is the
* standard separator for env variables. In order to represent dashes, two underscores
* are needed (and for one underscore use three underscores in the env variable).
*
* For example, the config key
* {{{
* docspell.server.app-name
* }}}
* can be given as env variable
* {{{
* DOCSPELL_SERVER_APP__NAME
* }}}
*/
object EnvConfig {
/** The config from current environment. */
lazy val get: Config =
loadFrom(System.getenv().asScala.view)
def loadFrom(env: MapView[String, String]): Config = {
val cfg = new Properties()
for (key <- env.keySet if key.startsWith("DOCSPELL_"))
cfg.setProperty(envToProp(key), env(key))
ConfigFactory
.parseProperties(cfg)
.withFallback(ConfigFactory.defaultReference())
.resolve()
}
/** Docspell has all lowercase key names and uses snake case.
*
* So this converts to lowercase and then replaces underscores (like
* [[com.typesafe.config.ConfigFactory.systemEnvironmentOverrides()]]
*
* - 3 underscores -> `_` (underscore)
* - 2 underscores -> `-` (dash)
* - 1 underscore -> `.` (dot)
*/
private[config] def envToProp(v: String): String = {
val len = v.length
val buffer = new mutable.StringBuilder()
val underscoreMapping = Map(3 -> '_', 2 -> '-', 1 -> '.').withDefault(_ => '_')
@annotation.tailrec
def go(current: Int, underscores: Int): String =
if (current >= len) buffer.toString()
else
v.charAt(current) match {
case '_' => go(current + 1, underscores + 1)
case c =>
if (underscores > 0) {
buffer.append(underscoreMapping(underscores))
}
buffer.append(c.toLower)
go(current + 1, 0)
}
go(0, 0)
}
}

View File

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later * SPDX-License-Identifier: AGPL-3.0-or-later
*/ */
package docspell.common.config package docspell.config
import java.nio.file.{Path => JPath} import java.nio.file.{Path => JPath}
@ -15,12 +15,11 @@ import fs2.io.file.Path
import docspell.common._ import docspell.common._
import com.github.eikek.calev.CalEvent import com.github.eikek.calev.CalEvent
import pureconfig._ import pureconfig.ConfigReader
import pureconfig.error.{CannotConvert, FailureReason} import pureconfig.error.{CannotConvert, FailureReason}
import scodec.bits.ByteVector import scodec.bits.ByteVector
object Implicits { object Implicits {
implicit val accountIdReader: ConfigReader[AccountId] = implicit val accountIdReader: ConfigReader[AccountId] =
ConfigReader[String].emap(reason(AccountId.parse)) ConfigReader[String].emap(reason(AccountId.parse))

View File

@ -0,0 +1,5 @@
docspell.server {
bind {
port = 7880
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 2020 Eike K. & Contributors
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package docspell.config
import munit.FunSuite
class EnvConfigTest extends FunSuite {
test("convert underscores") {
assertEquals(EnvConfig.envToProp("A_B_C"), "a.b.c")
assertEquals(EnvConfig.envToProp("A_B__C"), "a.b-c")
assertEquals(EnvConfig.envToProp("AA_BB__CC___D"), "aa.bb-cc_d")
}
test("insert docspell keys") {
val cfg = EnvConfig.loadFrom(
Map(
"DOCSPELL_SERVER_APP__NAME" -> "Hello!",
"DOCSPELL_JOEX_BIND_PORT" -> "1234"
).view
)
assertEquals(cfg.getString("docspell.server.app-name"), "Hello!")
assertEquals(cfg.getInt("docspell.joex.bind.port"), 1234)
}
test("find default values from reference.conf") {
val cfg = EnvConfig.loadFrom(
Map(
"DOCSPELL_SERVER_APP__NAME" -> "Hello!",
"DOCSPELL_JOEX_BIND_PORT" -> "1234"
).view
)
assertEquals(cfg.getInt("docspell.server.bind.port"), 7880)
}
test("discard non docspell keys") {
val cfg = EnvConfig.loadFrom(Map("A_B_C" -> "12").view)
assert(!cfg.hasPath("a.b.c"))
}
}

View File

@ -20,14 +20,19 @@ docspell.joex {
# The database connection. # The database connection.
# #
# By default a H2 file-based database is configured. You can provide
# a postgresql or mariadb connection here. When using H2 use the
# PostgreSQL compatibility mode and AUTO_SERVER feature.
#
# It must be the same connection as the rest server is using. # It must be the same connection as the rest server is using.
jdbc { jdbc {
# The JDBC url to the database. By default a H2 file-based
# database is configured. You can provide a postgresql or mariadb
# connection here. When using H2 use the PostgreSQL compatibility
# mode and AUTO_SERVER feature.
url = "jdbc:h2://"${java.io.tmpdir}"/docspell-demo.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;AUTO_SERVER=TRUE" url = "jdbc:h2://"${java.io.tmpdir}"/docspell-demo.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;AUTO_SERVER=TRUE"
# The database user.
user = "sa" user = "sa"
# The database password.
password = "" password = ""
} }

View File

@ -8,9 +8,12 @@ package docspell.joex
import cats.data.Validated import cats.data.Validated
import cats.data.ValidatedNec import cats.data.ValidatedNec
import cats.effect.Async
import cats.implicits._ import cats.implicits._
import docspell.common.config.Implicits._ import docspell.common.Logger
import docspell.config.ConfigFactory
import docspell.config.Implicits._
import docspell.joex.scheduler.CountingScheme import docspell.joex.scheduler.CountingScheme
import emil.MailAddress import emil.MailAddress
@ -22,8 +25,12 @@ import yamusca.imports._
object ConfigFile { object ConfigFile {
import Implicits._ import Implicits._
def loadConfig: Config = def loadConfig[F[_]: Async](args: List[String]): F[Config] = {
validOrThrow(ConfigSource.default.at("docspell.joex").loadOrThrow[Config]) val logger = Logger.log4s[F](org.log4s.getLogger)
ConfigFactory
.default[F, Config](logger, "docspell.joex")(args)
.map(cfg => validOrThrow(cfg))
}
private def validOrThrow(cfg: Config): Config = private def validOrThrow(cfg: Config): Config =
validate(cfg).fold(err => sys.error(err.toList.mkString("- ", "\n", "")), identity) validate(cfg).fold(err => sys.error(err.toList.mkString("- ", "\n", "")), identity)

View File

@ -6,46 +6,23 @@
package docspell.joex package docspell.joex
import java.nio.file.{Files, Paths}
import cats.effect._ import cats.effect._
import cats.implicits._
import docspell.common._ import docspell.common._
import org.log4s._ import org.log4s.getLogger
object Main extends IOApp { object Main extends IOApp {
private[this] val logger = getLogger
val blockingEC = private val logger: Logger[IO] = Logger.log4s[IO](getLogger)
ThreadFactories.cached[IO](ThreadFactories.ofName("docspell-joex-blocking"))
val connectEC = private val connectEC =
ThreadFactories.fixed[IO](5, ThreadFactories.ofName("docspell-joex-dbconnect")) ThreadFactories.fixed[IO](5, ThreadFactories.ofName("docspell-joex-dbconnect"))
val restserverEC =
ThreadFactories.workSteal[IO](ThreadFactories.ofNameFJ("docspell-joex-server"))
def run(args: List[String]) = { def run(args: List[String]): IO[ExitCode] =
args match { for {
case file :: Nil => cfg <- ConfigFile.loadConfig[IO](args)
val path = Paths.get(file).toAbsolutePath.normalize banner = Banner(
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 = ConfigFile.loadConfig
val banner = Banner(
"JOEX", "JOEX",
BuildInfo.version, BuildInfo.version,
BuildInfo.gitHeadCommit, BuildInfo.gitHeadCommit,
@ -55,18 +32,19 @@ object Main extends IOApp {
cfg.baseUrl, cfg.baseUrl,
Some(cfg.fullTextSearch.solr.url).filter(_ => cfg.fullTextSearch.enabled) Some(cfg.fullTextSearch.solr.url).filter(_ => cfg.fullTextSearch.enabled)
) )
logger.info(s"\n${banner.render("***>")}") _ <- logger.info(s"\n${banner.render("***>")}")
_ <-
if (EnvMode.current.isDev) { if (EnvMode.current.isDev) {
logger.warn(">>>>> Docspell is running in DEV mode! <<<<<") logger.warn(">>>>> Docspell is running in DEV mode! <<<<<")
} } else IO(())
val pools = connectEC.map(Pools.apply) pools = connectEC.map(Pools.apply)
pools.use(p => rc <- pools.use(p =>
JoexServer JoexServer
.stream[IO](cfg, p) .stream[IO](cfg, p)
.compile .compile
.drain .drain
.as(ExitCode.Success) .as(ExitCode.Success)
) )
} } yield rc
} }

View File

@ -47,8 +47,9 @@ docspell.server {
# The secret for this server that is used to sign the authenicator # The secret for this server that is used to sign the authenicator
# tokens. If multiple servers are running, all must share the same # tokens. If multiple servers are running, all must share the same
# secret. You can use base64 or hex strings (prefix with b64: and # secret. You can use base64 or hex strings (prefix with b64: and
# hex:, respectively). # hex:, respectively). If empty, a random secret is generated.
server-secret = "hex:caffee" # Example: b64:YRx77QujCGkHSvll0TVEmtTaw3Z5eXr+nWMsEJowgKg=
server-secret = ""
# How long an authentication token is valid. The web application # How long an authentication token is valid. The web application
# will get a new one periodically. # will get a new one periodically.
@ -279,6 +280,7 @@ docspell.server {
# Configuration for the backend. # Configuration for the backend.
backend { backend {
# Enable or disable debugging for e-mail related functionality. This # Enable or disable debugging for e-mail related functionality. This
# applies to both sending and receiving mails. For security reasons # applies to both sending and receiving mails. For security reasons
# logging is not very extensive on authentication failures. Setting # logging is not very extensive on authentication failures. Setting
@ -286,13 +288,17 @@ docspell.server {
mail-debug = false mail-debug = false
# The database connection. # The database connection.
#
# By default a H2 file-based database is configured. You can
# provide a postgresql or mariadb connection here. When using H2
# use the PostgreSQL compatibility mode and AUTO_SERVER feature.
jdbc { jdbc {
# The JDBC url to the database. By default a H2 file-based
# database is configured. You can provide a postgresql or
# mariadb connection here. When using H2 use the PostgreSQL
# compatibility mode and AUTO_SERVER feature.
url = "jdbc:h2://"${java.io.tmpdir}"/docspell-demo.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;AUTO_SERVER=TRUE" url = "jdbc:h2://"${java.io.tmpdir}"/docspell-demo.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;AUTO_SERVER=TRUE"
# The database user.
user = "sa" user = "sa"
# The database password.
password = "" password = ""
} }

View File

@ -6,23 +6,34 @@
package docspell.restserver package docspell.restserver
import java.security.SecureRandom
import cats.Semigroup import cats.Semigroup
import cats.data.{Validated, ValidatedNec} import cats.data.{Validated, ValidatedNec}
import cats.effect.Async
import cats.implicits._ import cats.implicits._
import docspell.backend.signup.{Config => SignupConfig} import docspell.backend.signup.{Config => SignupConfig}
import docspell.common.config.Implicits._ import docspell.common.Logger
import docspell.config.ConfigFactory
import docspell.config.Implicits._
import docspell.oidc.{ProviderConfig, SignatureAlgo} import docspell.oidc.{ProviderConfig, SignatureAlgo}
import docspell.restserver.auth.OpenId import docspell.restserver.auth.OpenId
import pureconfig._ import pureconfig._
import pureconfig.generic.auto._ import pureconfig.generic.auto._
import scodec.bits.ByteVector
object ConfigFile { object ConfigFile {
private[this] val unsafeLogger = org.log4s.getLogger
import Implicits._ import Implicits._
def loadConfig: Config = def loadConfig[F[_]: Async](args: List[String]): F[Config] = {
Validate(ConfigSource.default.at("docspell.server").loadOrThrow[Config]) val logger = Logger.log4s(unsafeLogger)
ConfigFactory
.default[F, Config](logger, "docspell.server")(args)
.map(cfg => Validate(cfg))
}
object Implicits { object Implicits {
implicit val signupModeReader: ConfigReader[SignupConfig.Mode] = implicit val signupModeReader: ConfigReader[SignupConfig.Mode] =
@ -50,12 +61,25 @@ object ConfigFile {
def all(cfg: Config) = List( def all(cfg: Config) = List(
duplicateOpenIdProvider(cfg), duplicateOpenIdProvider(cfg),
signKeyVsUserUrl(cfg) signKeyVsUserUrl(cfg),
generateSecretIfEmpty(cfg)
) )
private def valid(cfg: Config): ValidatedNec[String, Config] = private def valid(cfg: Config): ValidatedNec[String, Config] =
Validated.validNec(cfg) Validated.validNec(cfg)
def generateSecretIfEmpty(cfg: Config): ValidatedNec[String, Config] =
if (cfg.auth.serverSecret.isEmpty) {
unsafeLogger.warn(
"No serverSecret specified. Generating a random one. It is recommended to add a server-secret in the config file."
)
val random = new SecureRandom()
val buffer = new Array[Byte](32)
random.nextBytes(buffer)
val secret = ByteVector.view(buffer)
valid(cfg.copy(auth = cfg.auth.copy(serverSecret = secret)))
} else valid(cfg)
def duplicateOpenIdProvider(cfg: Config): ValidatedNec[String, Config] = { def duplicateOpenIdProvider(cfg: Config): ValidatedNec[String, Config] = {
val dupes = val dupes =
cfg.openid cfg.openid

View File

@ -6,46 +6,21 @@
package docspell.restserver package docspell.restserver
import java.nio.file.{Files, Paths}
import cats.effect._ import cats.effect._
import cats.implicits._
import docspell.common._ import docspell.common._
import org.log4s._ import org.log4s.getLogger
object Main extends IOApp { object Main extends IOApp {
private[this] val logger = getLogger private[this] val logger: Logger[IO] = Logger.log4s(getLogger)
val blockingEC = private val connectEC =
ThreadFactories.cached[IO](ThreadFactories.ofName("docspell-restserver-blocking"))
val connectEC =
ThreadFactories.fixed[IO](5, ThreadFactories.ofName("docspell-dbconnect")) ThreadFactories.fixed[IO](5, ThreadFactories.ofName("docspell-dbconnect"))
val restserverEC =
ThreadFactories.workSteal[IO](ThreadFactories.ofNameFJ("docspell-restserver"))
def run(args: List[String]) = { def run(args: List[String]) = for {
args match { cfg <- ConfigFile.loadConfig[IO](args)
case file :: Nil => banner = Banner(
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 = ConfigFile.loadConfig
val banner = Banner(
"REST Server", "REST Server",
BuildInfo.version, BuildInfo.version,
BuildInfo.gitHeadCommit, BuildInfo.gitHeadCommit,
@ -55,12 +30,14 @@ object Main extends IOApp {
cfg.baseUrl, cfg.baseUrl,
Some(cfg.fullTextSearch.solr.url).filter(_ => cfg.fullTextSearch.enabled) Some(cfg.fullTextSearch.solr.url).filter(_ => cfg.fullTextSearch.enabled)
) )
val pools = connectEC.map(Pools.apply) _ <- logger.info(s"\n${banner.render("***>")}")
logger.info(s"\n${banner.render("***>")}") _ <-
if (EnvMode.current.isDev) { if (EnvMode.current.isDev) {
logger.warn(">>>>> Docspell is running in DEV mode! <<<<<") logger.warn(">>>>> Docspell is running in DEV mode! <<<<<")
} } else IO(())
pools = connectEC.map(Pools.apply)
rc <-
pools.use(p => pools.use(p =>
RestServer RestServer
.stream[IO](cfg, p) .stream[IO](cfg, p)
@ -68,5 +45,5 @@ object Main extends IOApp {
.drain .drain
.as(ExitCode.Success) .as(ExitCode.Success)
) )
} } yield rc
} }

75
project/EnvConfig.scala Normal file
View File

@ -0,0 +1,75 @@
import sbt._
import com.typesafe.config._
import scala.annotation.tailrec
import scala.jdk.CollectionConverters._
import java.util.{Map => JMap}
object EnvConfig {
def serializeTo(cfg: Config, out: File): Unit =
IO.write(out, serialize(cfg))
def serialize(cfg: Config): String = {
val buffer = new StringBuilder
buffer.append("#### Server Configuration ####\n")
for (
entry <- cfg.entrySet().asScala.toList.sortBy(_.getKey)
if isValidKey("docspell.server", entry)
) append(buffer, entry.getKey, entry.getValue)
buffer.append("\n#### JOEX Configuration ####\n")
for (
entry <- cfg.entrySet().asScala.toList.sortBy(_.getKey)
if isValidKey("docspell.joex", entry)
) append(buffer, entry.getKey, entry.getValue)
buffer.toString().trim
}
private def append(buffer: StringBuilder, key: String, value: ConfigValue): Unit = {
if (value.origin().comments().asScala.nonEmpty) {
buffer.append("\n")
}
value
.origin()
.comments()
.forEach(c => buffer.append("# ").append(c).append("\n"))
buffer.append(keyToEnv(key)).append("=").append(value.render()).append("\n")
}
def isValidKey(prefix: String, entry: JMap.Entry[String, ConfigValue]): Boolean =
entry.getKey
.startsWith(prefix) && entry.getValue.valueType() != ConfigValueType.LIST
def makeConfig(files: List[File]): Config =
files
.foldLeft(ConfigFactory.empty()) { (cfg, file) =>
val cf = ConfigFactory.parseFile(file)
cfg.withFallback(cf)
}
.withFallback(ConfigFactory.defaultOverrides(getClass.getClassLoader))
.resolve()
def makeConfig(file: File, files: File*): Config =
makeConfig(file :: files.toList)
def keyToEnv(k: String): String = {
val buffer = new StringBuilder()
val len = k.length
@tailrec
def go(current: Int): String =
if (current >= len) buffer.toString()
else {
k.charAt(current) match {
case '.' => buffer.append("_")
case '-' => buffer.append("__")
case '_' => buffer.append("___")
case c => buffer.append(c.toUpper)
}
go(current + 1)
}
go(0)
}
}

2
project/build.sbt Normal file
View File

@ -0,0 +1,2 @@
libraryDependencies ++=
Seq("com.typesafe" % "config" % "1.4.1")

View File

@ -8,10 +8,11 @@ mktoc = true
+++ +++
Docspell's executables (restserver and joex) can take one argument a Docspell's executables (restserver and joex) can take one argument a
configuration file. If that is not given, the defaults are used. The configuration file. If that is not given, the defaults are used,
config file overrides default values, so only values that differ from overriden by environment variables. A config file overrides default
the defaults are necessary. The complete default options and their values, so only values that differ from the defaults are necessary.
documentation is at the end of this page. The complete default options and their documentation is at the end of
this page.
Besides the config file, another way is to provide individual settings Besides the config file, another way is to provide individual settings
via key-value pairs to the executable by the `-D` option. For example via key-value pairs to the executable by the `-D` option. For example
@ -22,6 +23,21 @@ the recommended way is to maintain a config file. If these options
*and* a file is provded, then any setting given via the `-D…` option *and* a file is provded, then any setting given via the `-D…` option
overrides the same setting from the config file. overrides the same setting from the config file.
At last, it is possible to configure docspell via environment
variables if there is no config file supplied (if a config file *is*
supplied, it is always preferred). Note that this approach is limited,
as arrays are not supported. A list of environment variables can be
found at the [end of this page](#environment-variables). The
environment variable name follows the corresponding config key - where
dots are replaced by underscores and dashes are replaced by two
underscores. For example, the config key `docspell.server.app-name`
can be defined as env variable `DOCSPELL_SERVER_APP__NAME`.
It is also possible to specify environment variables inside a config
file (to get a mix of both) - please see the [documentation of the
config library](https://github.com/lightbend/config#standard-behavior)
for more on this.
# File Format # File Format
The format of the configuration files can be The format of the configuration files can be
@ -31,9 +47,9 @@ library](https://github.com/lightbend/config) understands. The default
values below are in HOCON format, which is recommended, since it values below are in HOCON format, which is recommended, since it
allows comments and has some [advanced allows comments and has some [advanced
features](https://github.com/lightbend/config#features-of-hocon). features](https://github.com/lightbend/config#features-of-hocon).
Please refer to their documentation for more on this. Please also see their documentation for more details.
A short description (please see the links for better understanding): A short description (please check the links for better understanding):
The config consists of key-value pairs and can be written in a The config consists of key-value pairs and can be written in a
JSON-like format (called HOCON). Keys are organized in trees, and a JSON-like format (called HOCON). Keys are organized in trees, and a
key defines a full path into the tree. There are two ways: key defines a full path into the tree. There are two ways:
@ -633,3 +649,11 @@ statements with level "DEBUG" will be printed, too.
{{ incl_conf(path="templates/shortcodes/joex.conf") }} {{ incl_conf(path="templates/shortcodes/joex.conf") }}
## Environment Variables
Environment variables can be used when there is no config file
supplied. The listing below shows all possible variables and their
default values.
{{ incl_conf(path="templates/shortcodes/config.env.txt") }}