mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-10-30 21:40:12 +00:00 
			
		
		
		
	Merge pull request #1135 from eikek/improvement/1121-config
Improvement/1121 config
This commit is contained in:
		
							
								
								
									
										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") }} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user