mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-05 19:09:32 +00:00
Merge pull request #1135 from eikek/improvement/1121-config
Improvement/1121 config
This commit is contained in:
commit
9af61cb4a4
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,5 +14,6 @@ _site/
|
||||
/website/site/templates/shortcodes/server.conf
|
||||
/website/site/templates/shortcodes/sample-exim.conf
|
||||
/website/site/templates/shortcodes/joex.conf
|
||||
/website/site/templates/shortcodes/config.env.txt
|
||||
/docker/docs
|
||||
/docker/dev-log
|
||||
|
@ -2,6 +2,7 @@ version = "3.0.6"
|
||||
|
||||
preset = default
|
||||
align.preset = some
|
||||
runner.dialect = scala213
|
||||
|
||||
maxColumn = 90
|
||||
|
||||
|
44
build.sbt
44
build.sbt
@ -293,10 +293,23 @@ val common = project
|
||||
Dependencies.circe ++
|
||||
Dependencies.loggingApi ++
|
||||
Dependencies.calevCore ++
|
||||
Dependencies.calevCirce ++
|
||||
Dependencies.pureconfig.map(_ % "optional")
|
||||
Dependencies.calevCirce
|
||||
)
|
||||
|
||||
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
|
||||
// https://file-examples.com/index.php/sample-documents-download/sample-doc-download/
|
||||
val files = project
|
||||
@ -603,7 +616,17 @@ val joex = project
|
||||
),
|
||||
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
|
||||
.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
|
||||
|
||||
@ -686,7 +709,6 @@ val website = project
|
||||
val templateOut = baseDirectory.value / "site" / "templates" / "shortcodes"
|
||||
val staticOut = baseDirectory.value / "site" / "static" / "openapi"
|
||||
IO.createDirectories(Seq(templateOut, staticOut))
|
||||
val logger = streams.value.log
|
||||
|
||||
val files = Seq(
|
||||
(restserver / Compile / resourceDirectory).value / "reference.conf" -> templateOut / "server.conf",
|
||||
@ -698,6 +720,17 @@ val website = project
|
||||
IO.copy(files)
|
||||
files.map(_._2)
|
||||
}.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 {
|
||||
val changelog = (LocalRootProject / baseDirectory).value / "Changelog.md"
|
||||
val targetDir = baseDirectory.value / "site" / "content" / "docs" / "changelog"
|
||||
@ -731,6 +764,7 @@ val root = project
|
||||
)
|
||||
.aggregate(
|
||||
common,
|
||||
config,
|
||||
extract,
|
||||
convert,
|
||||
analysis,
|
||||
|
@ -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
|
||||
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.common.config
|
||||
package docspell.config
|
||||
|
||||
import java.nio.file.{Path => JPath}
|
||||
|
||||
@ -15,12 +15,11 @@ import fs2.io.file.Path
|
||||
import docspell.common._
|
||||
|
||||
import com.github.eikek.calev.CalEvent
|
||||
import pureconfig._
|
||||
import pureconfig.ConfigReader
|
||||
import pureconfig.error.{CannotConvert, FailureReason}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
object Implicits {
|
||||
|
||||
implicit val accountIdReader: ConfigReader[AccountId] =
|
||||
ConfigReader[String].emap(reason(AccountId.parse))
|
||||
|
5
modules/config/src/test/resources/reference.conf
Normal file
5
modules/config/src/test/resources/reference.conf
Normal file
@ -0,0 +1,5 @@
|
||||
docspell.server {
|
||||
bind {
|
||||
port = 7880
|
||||
}
|
||||
}
|
@ -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"))
|
||||
}
|
||||
}
|
@ -20,14 +20,19 @@ docspell.joex {
|
||||
|
||||
# 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.
|
||||
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"
|
||||
|
||||
# The database user.
|
||||
user = "sa"
|
||||
|
||||
# The database password.
|
||||
password = ""
|
||||
}
|
||||
|
||||
|
@ -8,9 +8,12 @@ package docspell.joex
|
||||
|
||||
import cats.data.Validated
|
||||
import cats.data.ValidatedNec
|
||||
import cats.effect.Async
|
||||
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 emil.MailAddress
|
||||
@ -22,8 +25,12 @@ import yamusca.imports._
|
||||
object ConfigFile {
|
||||
import Implicits._
|
||||
|
||||
def loadConfig: Config =
|
||||
validOrThrow(ConfigSource.default.at("docspell.joex").loadOrThrow[Config])
|
||||
def loadConfig[F[_]: Async](args: List[String]): F[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 =
|
||||
validate(cfg).fold(err => sys.error(err.toList.mkString("- ", "\n", "")), identity)
|
||||
|
@ -6,67 +6,45 @@
|
||||
|
||||
package docspell.joex
|
||||
|
||||
import java.nio.file.{Files, Paths}
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
|
||||
import org.log4s._
|
||||
import org.log4s.getLogger
|
||||
|
||||
object Main extends IOApp {
|
||||
private[this] val logger = getLogger
|
||||
|
||||
val blockingEC =
|
||||
ThreadFactories.cached[IO](ThreadFactories.ofName("docspell-joex-blocking"))
|
||||
val connectEC =
|
||||
private val logger: Logger[IO] = Logger.log4s[IO](getLogger)
|
||||
|
||||
private val connectEC =
|
||||
ThreadFactories.fixed[IO](5, ThreadFactories.ofName("docspell-joex-dbconnect"))
|
||||
val restserverEC =
|
||||
ThreadFactories.workSteal[IO](ThreadFactories.ofNameFJ("docspell-joex-server"))
|
||||
|
||||
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 _ =>
|
||||
}
|
||||
}
|
||||
def run(args: List[String]): IO[ExitCode] =
|
||||
for {
|
||||
cfg <- ConfigFile.loadConfig[IO](args)
|
||||
banner = Banner(
|
||||
"JOEX",
|
||||
BuildInfo.version,
|
||||
BuildInfo.gitHeadCommit,
|
||||
cfg.jdbc.url,
|
||||
Option(System.getProperty("config.file")),
|
||||
cfg.appId,
|
||||
cfg.baseUrl,
|
||||
Some(cfg.fullTextSearch.solr.url).filter(_ => cfg.fullTextSearch.enabled)
|
||||
)
|
||||
_ <- logger.info(s"\n${banner.render("***>")}")
|
||||
_ <-
|
||||
if (EnvMode.current.isDev) {
|
||||
logger.warn(">>>>> Docspell is running in DEV mode! <<<<<")
|
||||
} else IO(())
|
||||
|
||||
val cfg = ConfigFile.loadConfig
|
||||
val banner = Banner(
|
||||
"JOEX",
|
||||
BuildInfo.version,
|
||||
BuildInfo.gitHeadCommit,
|
||||
cfg.jdbc.url,
|
||||
Option(System.getProperty("config.file")),
|
||||
cfg.appId,
|
||||
cfg.baseUrl,
|
||||
Some(cfg.fullTextSearch.solr.url).filter(_ => cfg.fullTextSearch.enabled)
|
||||
)
|
||||
logger.info(s"\n${banner.render("***>")}")
|
||||
if (EnvMode.current.isDev) {
|
||||
logger.warn(">>>>> Docspell is running in DEV mode! <<<<<")
|
||||
}
|
||||
|
||||
val pools = connectEC.map(Pools.apply)
|
||||
pools.use(p =>
|
||||
JoexServer
|
||||
.stream[IO](cfg, p)
|
||||
.compile
|
||||
.drain
|
||||
.as(ExitCode.Success)
|
||||
)
|
||||
}
|
||||
pools = connectEC.map(Pools.apply)
|
||||
rc <- pools.use(p =>
|
||||
JoexServer
|
||||
.stream[IO](cfg, p)
|
||||
.compile
|
||||
.drain
|
||||
.as(ExitCode.Success)
|
||||
)
|
||||
} yield rc
|
||||
}
|
||||
|
@ -47,8 +47,9 @@ docspell.server {
|
||||
# The secret for this server that is used to sign the authenicator
|
||||
# tokens. If multiple servers are running, all must share the same
|
||||
# secret. You can use base64 or hex strings (prefix with b64: and
|
||||
# hex:, respectively).
|
||||
server-secret = "hex:caffee"
|
||||
# hex:, respectively). If empty, a random secret is generated.
|
||||
# Example: b64:YRx77QujCGkHSvll0TVEmtTaw3Z5eXr+nWMsEJowgKg=
|
||||
server-secret = ""
|
||||
|
||||
# How long an authentication token is valid. The web application
|
||||
# will get a new one periodically.
|
||||
@ -279,6 +280,7 @@ docspell.server {
|
||||
|
||||
# Configuration for the backend.
|
||||
backend {
|
||||
|
||||
# Enable or disable debugging for e-mail related functionality. This
|
||||
# applies to both sending and receiving mails. For security reasons
|
||||
# logging is not very extensive on authentication failures. Setting
|
||||
@ -286,13 +288,17 @@ docspell.server {
|
||||
mail-debug = false
|
||||
|
||||
# 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 {
|
||||
# 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"
|
||||
|
||||
# The database user.
|
||||
user = "sa"
|
||||
|
||||
# The database password.
|
||||
password = ""
|
||||
}
|
||||
|
||||
|
@ -6,23 +6,34 @@
|
||||
|
||||
package docspell.restserver
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
import cats.Semigroup
|
||||
import cats.data.{Validated, ValidatedNec}
|
||||
import cats.effect.Async
|
||||
import cats.implicits._
|
||||
|
||||
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.restserver.auth.OpenId
|
||||
|
||||
import pureconfig._
|
||||
import pureconfig.generic.auto._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
object ConfigFile {
|
||||
private[this] val unsafeLogger = org.log4s.getLogger
|
||||
import Implicits._
|
||||
|
||||
def loadConfig: Config =
|
||||
Validate(ConfigSource.default.at("docspell.server").loadOrThrow[Config])
|
||||
def loadConfig[F[_]: Async](args: List[String]): F[Config] = {
|
||||
val logger = Logger.log4s(unsafeLogger)
|
||||
ConfigFactory
|
||||
.default[F, Config](logger, "docspell.server")(args)
|
||||
.map(cfg => Validate(cfg))
|
||||
}
|
||||
|
||||
object Implicits {
|
||||
implicit val signupModeReader: ConfigReader[SignupConfig.Mode] =
|
||||
@ -50,12 +61,25 @@ object ConfigFile {
|
||||
|
||||
def all(cfg: Config) = List(
|
||||
duplicateOpenIdProvider(cfg),
|
||||
signKeyVsUserUrl(cfg)
|
||||
signKeyVsUserUrl(cfg),
|
||||
generateSecretIfEmpty(cfg)
|
||||
)
|
||||
|
||||
private def valid(cfg: Config): ValidatedNec[String, Config] =
|
||||
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] = {
|
||||
val dupes =
|
||||
cfg.openid
|
||||
|
@ -6,46 +6,21 @@
|
||||
|
||||
package docspell.restserver
|
||||
|
||||
import java.nio.file.{Files, Paths}
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
|
||||
import org.log4s._
|
||||
import org.log4s.getLogger
|
||||
|
||||
object Main extends IOApp {
|
||||
private[this] val logger = getLogger
|
||||
private[this] val logger: Logger[IO] = Logger.log4s(getLogger)
|
||||
|
||||
val blockingEC =
|
||||
ThreadFactories.cached[IO](ThreadFactories.ofName("docspell-restserver-blocking"))
|
||||
val connectEC =
|
||||
private val connectEC =
|
||||
ThreadFactories.fixed[IO](5, ThreadFactories.ofName("docspell-dbconnect"))
|
||||
val restserverEC =
|
||||
ThreadFactories.workSteal[IO](ThreadFactories.ofNameFJ("docspell-restserver"))
|
||||
|
||||
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 = ConfigFile.loadConfig
|
||||
val banner = Banner(
|
||||
def run(args: List[String]) = for {
|
||||
cfg <- ConfigFile.loadConfig[IO](args)
|
||||
banner = Banner(
|
||||
"REST Server",
|
||||
BuildInfo.version,
|
||||
BuildInfo.gitHeadCommit,
|
||||
@ -55,18 +30,20 @@ object Main extends IOApp {
|
||||
cfg.baseUrl,
|
||||
Some(cfg.fullTextSearch.solr.url).filter(_ => cfg.fullTextSearch.enabled)
|
||||
)
|
||||
val pools = connectEC.map(Pools.apply)
|
||||
logger.info(s"\n${banner.render("***>")}")
|
||||
if (EnvMode.current.isDev) {
|
||||
logger.warn(">>>>> Docspell is running in DEV mode! <<<<<")
|
||||
}
|
||||
_ <- logger.info(s"\n${banner.render("***>")}")
|
||||
_ <-
|
||||
if (EnvMode.current.isDev) {
|
||||
logger.warn(">>>>> Docspell is running in DEV mode! <<<<<")
|
||||
} else IO(())
|
||||
|
||||
pools.use(p =>
|
||||
RestServer
|
||||
.stream[IO](cfg, p)
|
||||
.compile
|
||||
.drain
|
||||
.as(ExitCode.Success)
|
||||
)
|
||||
}
|
||||
pools = connectEC.map(Pools.apply)
|
||||
rc <-
|
||||
pools.use(p =>
|
||||
RestServer
|
||||
.stream[IO](cfg, p)
|
||||
.compile
|
||||
.drain
|
||||
.as(ExitCode.Success)
|
||||
)
|
||||
} yield rc
|
||||
}
|
||||
|
75
project/EnvConfig.scala
Normal file
75
project/EnvConfig.scala
Normal 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
2
project/build.sbt
Normal file
@ -0,0 +1,2 @@
|
||||
libraryDependencies ++=
|
||||
Seq("com.typesafe" % "config" % "1.4.1")
|
@ -8,10 +8,11 @@ mktoc = true
|
||||
+++
|
||||
|
||||
Docspell's executables (restserver and joex) can take one argument – a
|
||||
configuration file. If that is not given, the defaults are used. The
|
||||
config file overrides default values, so only values that differ from
|
||||
the defaults are necessary. The complete default options and their
|
||||
documentation is at the end of this page.
|
||||
configuration file. If that is not given, the defaults are used,
|
||||
overriden by environment variables. A config file overrides default
|
||||
values, so only values that differ from the defaults are necessary.
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
allows comments and has some [advanced
|
||||
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
|
||||
JSON-like format (called HOCON). Keys are organized in trees, and a
|
||||
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") }}
|
||||
|
||||
## 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") }}
|
||||
|
Loading…
x
Reference in New Issue
Block a user