mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-21 09:58:26 +00:00
@ -2,6 +2,7 @@ version = "3.0.6"
|
||||
|
||||
preset = default
|
||||
align.preset = some
|
||||
runner.dialect = scala213
|
||||
|
||||
maxColumn = 90
|
||||
|
||||
|
32
build.sbt
32
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
|
||||
|
||||
@ -731,6 +754,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"))
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -8,10 +8,13 @@ package docspell.restserver
|
||||
|
||||
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
|
||||
|
||||
@ -21,8 +24,12 @@ import pureconfig.generic.auto._
|
||||
object ConfigFile {
|
||||
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(org.log4s.getLogger)
|
||||
ConfigFactory
|
||||
.default[F, Config](logger, "docspell.server")(args)
|
||||
.map(cfg => Validate(cfg))
|
||||
}
|
||||
|
||||
object Implicits {
|
||||
implicit val signupModeReader: ConfigReader[SignupConfig.Mode] =
|
||||
|
@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user