mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-22 02:18:26 +00:00
Use different file stores based on config
This commit is contained in:
@ -6,9 +6,13 @@
|
|||||||
|
|
||||||
package docspell.backend
|
package docspell.backend
|
||||||
|
|
||||||
|
import cats.data.{Validated, ValidatedNec}
|
||||||
|
import cats.implicits._
|
||||||
|
|
||||||
import docspell.backend.signup.{Config => SignupConfig}
|
import docspell.backend.signup.{Config => SignupConfig}
|
||||||
import docspell.common._
|
import docspell.common._
|
||||||
import docspell.store.JdbcConfig
|
import docspell.store.JdbcConfig
|
||||||
|
import docspell.store.file.FileRepositoryConfig
|
||||||
|
|
||||||
import emil.javamail.Settings
|
import emil.javamail.Settings
|
||||||
|
|
||||||
@ -21,10 +25,49 @@ case class Config(
|
|||||||
|
|
||||||
def mailSettings: Settings =
|
def mailSettings: Settings =
|
||||||
Settings.defaultSettings.copy(debug = mailDebug)
|
Settings.defaultSettings.copy(debug = mailDebug)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Config {
|
object Config {
|
||||||
|
|
||||||
case class Files(chunkSize: Int, validMimeTypes: Seq[MimeType])
|
case class Files(
|
||||||
|
chunkSize: Int,
|
||||||
|
validMimeTypes: Seq[MimeType],
|
||||||
|
defaultStore: Ident,
|
||||||
|
stores: Map[Ident, FileStoreConfig]
|
||||||
|
) {
|
||||||
|
val enabledStores: Map[Ident, FileStoreConfig] =
|
||||||
|
stores.view.filter(_._2.enabled).toMap
|
||||||
|
|
||||||
|
def defaultStoreConfig: FileStoreConfig =
|
||||||
|
enabledStores(defaultStore)
|
||||||
|
|
||||||
|
def toFileRepositoryConfig: FileRepositoryConfig =
|
||||||
|
defaultStoreConfig match {
|
||||||
|
case FileStoreConfig.DefaultDatabase(_) =>
|
||||||
|
FileRepositoryConfig.Database(chunkSize)
|
||||||
|
case FileStoreConfig.S3(_, endpoint, accessKey, secretKey, bucket) =>
|
||||||
|
FileRepositoryConfig.S3(endpoint, accessKey, secretKey, bucket, chunkSize)
|
||||||
|
case FileStoreConfig.FileSystem(_, directory) =>
|
||||||
|
FileRepositoryConfig.Directory(directory, chunkSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
def validate: ValidatedNec[String, Files] = {
|
||||||
|
val storesEmpty =
|
||||||
|
if (enabledStores.isEmpty)
|
||||||
|
Validated.invalidNec(
|
||||||
|
"No file stores defined! Make sure at least one enabled store is present."
|
||||||
|
)
|
||||||
|
else Validated.validNec(())
|
||||||
|
|
||||||
|
val defaultStorePresent =
|
||||||
|
enabledStores.get(defaultStore) match {
|
||||||
|
case Some(_) => Validated.validNec(())
|
||||||
|
case None =>
|
||||||
|
Validated.invalidNec(s"Default file store not present: ${defaultStore.id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
(storesEmpty |+| defaultStorePresent).map(_ => this)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Eike K. & Contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package docspell.common
|
||||||
|
|
||||||
|
import fs2.io.file.Path
|
||||||
|
|
||||||
|
sealed trait FileStoreConfig {
|
||||||
|
def enabled: Boolean
|
||||||
|
def storeType: FileStoreType
|
||||||
|
}
|
||||||
|
object FileStoreConfig {
|
||||||
|
case class DefaultDatabase(enabled: Boolean) extends FileStoreConfig {
|
||||||
|
val storeType = FileStoreType.DefaultDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
case class FileSystem(
|
||||||
|
enabled: Boolean,
|
||||||
|
directory: Path
|
||||||
|
) extends FileStoreConfig {
|
||||||
|
val storeType = FileStoreType.FileSystem
|
||||||
|
}
|
||||||
|
|
||||||
|
case class S3(
|
||||||
|
enabled: Boolean,
|
||||||
|
endpoint: String,
|
||||||
|
accessKey: String,
|
||||||
|
secretKey: String,
|
||||||
|
bucket: String
|
||||||
|
) extends FileStoreConfig {
|
||||||
|
val storeType = FileStoreType.S3
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 Eike K. & Contributors
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package docspell.common
|
||||||
|
|
||||||
|
import cats.data.NonEmptyList
|
||||||
|
|
||||||
|
sealed trait FileStoreType { self: Product =>
|
||||||
|
def name: String =
|
||||||
|
productPrefix.toLowerCase
|
||||||
|
}
|
||||||
|
object FileStoreType {
|
||||||
|
case object DefaultDatabase extends FileStoreType
|
||||||
|
|
||||||
|
case object S3 extends FileStoreType
|
||||||
|
|
||||||
|
case object FileSystem extends FileStoreType
|
||||||
|
|
||||||
|
val all: NonEmptyList[FileStoreType] =
|
||||||
|
NonEmptyList.of(DefaultDatabase, S3, FileSystem)
|
||||||
|
|
||||||
|
def fromString(str: String): Either[String, FileStoreType] =
|
||||||
|
all
|
||||||
|
.find(_.name.equalsIgnoreCase(str))
|
||||||
|
.toRight(s"Invalid file store type: $str")
|
||||||
|
|
||||||
|
def unsafeFromString(str: String): FileStoreType =
|
||||||
|
fromString(str).fold(sys.error, identity)
|
||||||
|
}
|
@ -18,9 +18,18 @@ import docspell.logging.{Level, LogConfig}
|
|||||||
import com.github.eikek.calev.CalEvent
|
import com.github.eikek.calev.CalEvent
|
||||||
import pureconfig.ConfigReader
|
import pureconfig.ConfigReader
|
||||||
import pureconfig.error.{CannotConvert, FailureReason}
|
import pureconfig.error.{CannotConvert, FailureReason}
|
||||||
|
import pureconfig.generic.{CoproductHint, FieldCoproductHint}
|
||||||
import scodec.bits.ByteVector
|
import scodec.bits.ByteVector
|
||||||
|
|
||||||
object Implicits {
|
object Implicits {
|
||||||
|
// the value "s-3" looks strange, this is to allow to write "s3" in the config
|
||||||
|
implicit val fileStoreCoproductHint: CoproductHint[FileStoreConfig] =
|
||||||
|
new FieldCoproductHint[FileStoreConfig]("type") {
|
||||||
|
override def fieldValue(name: String) =
|
||||||
|
if (name.equalsIgnoreCase("S3")) "s3"
|
||||||
|
else super.fieldValue(name)
|
||||||
|
}
|
||||||
|
|
||||||
implicit val accountIdReader: ConfigReader[AccountId] =
|
implicit val accountIdReader: ConfigReader[AccountId] =
|
||||||
ConfigReader[String].emap(reason(AccountId.parse))
|
ConfigReader[String].emap(reason(AccountId.parse))
|
||||||
|
|
||||||
@ -42,6 +51,9 @@ object Implicits {
|
|||||||
implicit val identReader: ConfigReader[Ident] =
|
implicit val identReader: ConfigReader[Ident] =
|
||||||
ConfigReader[String].emap(reason(Ident.fromString))
|
ConfigReader[String].emap(reason(Ident.fromString))
|
||||||
|
|
||||||
|
implicit def identMapReader[B: ConfigReader]: ConfigReader[Map[Ident, B]] =
|
||||||
|
pureconfig.configurable.genericMapReader[Ident, B](reason(Ident.fromString))
|
||||||
|
|
||||||
implicit val byteVectorReader: ConfigReader[ByteVector] =
|
implicit val byteVectorReader: ConfigReader[ByteVector] =
|
||||||
ConfigReader[String].emap(reason { str =>
|
ConfigReader[String].emap(reason { str =>
|
||||||
if (str.startsWith("hex:"))
|
if (str.startsWith("hex:"))
|
||||||
@ -70,6 +82,9 @@ object Implicits {
|
|||||||
implicit val logLevelReader: ConfigReader[Level] =
|
implicit val logLevelReader: ConfigReader[Level] =
|
||||||
ConfigReader[String].emap(reason(Level.fromString))
|
ConfigReader[String].emap(reason(Level.fromString))
|
||||||
|
|
||||||
|
implicit val fileStoreTypeReader: ConfigReader[FileStoreType] =
|
||||||
|
ConfigReader[String].emap(reason(FileStoreType.fromString))
|
||||||
|
|
||||||
def reason[A: ClassTag](
|
def reason[A: ClassTag](
|
||||||
f: String => Either[String, A]
|
f: String => Either[String, A]
|
||||||
): String => Either[FailureReason, A] =
|
): String => Either[FailureReason, A] =
|
||||||
|
@ -646,6 +646,41 @@ Docpell Update Check
|
|||||||
# restrict file types that should be handed over to processing.
|
# restrict file types that should be handed over to processing.
|
||||||
# By default all files are allowed.
|
# By default all files are allowed.
|
||||||
valid-mime-types = [ ]
|
valid-mime-types = [ ]
|
||||||
|
|
||||||
|
# The id of an enabled store from the `stores` array that should
|
||||||
|
# be used.
|
||||||
|
#
|
||||||
|
# IMPORTANT NOTE: All nodes must have the exact same file store
|
||||||
|
# configuration!
|
||||||
|
default-store = "database"
|
||||||
|
|
||||||
|
# A list of possible file stores. Each entry must have a unique
|
||||||
|
# id. The `type` is one of: default-database, filesystem, s3.
|
||||||
|
#
|
||||||
|
# The enabled property serves currently to define target stores
|
||||||
|
# for te "copy files" task. All stores with enabled=false are
|
||||||
|
# removed from the list. The `default-store` must be enabled.
|
||||||
|
stores = {
|
||||||
|
database =
|
||||||
|
{ enabled = true
|
||||||
|
type = "default-database"
|
||||||
|
}
|
||||||
|
|
||||||
|
filesystem =
|
||||||
|
{ enabled = false
|
||||||
|
type = "file-system"
|
||||||
|
directory = "/some/directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
minio =
|
||||||
|
{ enabled = false
|
||||||
|
type = "s3"
|
||||||
|
endpoint = "http://localhost:9000"
|
||||||
|
access-key = "username"
|
||||||
|
secret-key = "password"
|
||||||
|
bucket = "docspell"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configuration of the full-text search engine.
|
# Configuration of the full-text search engine.
|
||||||
|
@ -19,8 +19,6 @@ import pureconfig.generic.auto._
|
|||||||
import yamusca.imports._
|
import yamusca.imports._
|
||||||
|
|
||||||
object ConfigFile {
|
object ConfigFile {
|
||||||
import Implicits._
|
|
||||||
|
|
||||||
def loadConfig[F[_]: Async](args: List[String]): F[Config] = {
|
def loadConfig[F[_]: Async](args: List[String]): F[Config] = {
|
||||||
val logger = docspell.logging.getLogger[F]
|
val logger = docspell.logging.getLogger[F]
|
||||||
ConfigFactory
|
ConfigFactory
|
||||||
@ -51,6 +49,7 @@ object ConfigFile {
|
|||||||
Validation.failWhen(
|
Validation.failWhen(
|
||||||
cfg => cfg.updateCheck.enabled && cfg.updateCheck.subject.els.isEmpty,
|
cfg => cfg.updateCheck.enabled && cfg.updateCheck.subject.els.isEmpty,
|
||||||
"No subject given for enabled update check!"
|
"No subject given for enabled update check!"
|
||||||
)
|
),
|
||||||
|
Validation(cfg => cfg.files.validate.map(_ => cfg))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ import docspell.common.Pools
|
|||||||
import docspell.joex.routes._
|
import docspell.joex.routes._
|
||||||
import docspell.pubsub.naive.NaivePubSub
|
import docspell.pubsub.naive.NaivePubSub
|
||||||
import docspell.store.Store
|
import docspell.store.Store
|
||||||
import docspell.store.file.FileRepositoryConfig
|
|
||||||
import docspell.store.records.RInternalSetting
|
import docspell.store.records.RInternalSetting
|
||||||
|
|
||||||
import org.http4s.HttpApp
|
import org.http4s.HttpApp
|
||||||
@ -42,7 +41,7 @@ object JoexServer {
|
|||||||
|
|
||||||
store <- Store.create[F](
|
store <- Store.create[F](
|
||||||
cfg.jdbc,
|
cfg.jdbc,
|
||||||
FileRepositoryConfig.Database(cfg.files.chunkSize),
|
cfg.files.toFileRepositoryConfig,
|
||||||
pools.connectEC
|
pools.connectEC
|
||||||
)
|
)
|
||||||
settings <- Resource.eval(store.transact(RInternalSetting.create))
|
settings <- Resource.eval(store.transact(RInternalSetting.create))
|
||||||
|
@ -358,6 +358,41 @@ docspell.server {
|
|||||||
# restrict file types that should be handed over to processing.
|
# restrict file types that should be handed over to processing.
|
||||||
# By default all files are allowed.
|
# By default all files are allowed.
|
||||||
valid-mime-types = [ ]
|
valid-mime-types = [ ]
|
||||||
|
|
||||||
|
# The id of an enabled store from the `stores` array that should
|
||||||
|
# be used.
|
||||||
|
#
|
||||||
|
# IMPORTANT NOTE: All nodes must have the exact same file store
|
||||||
|
# configuration!
|
||||||
|
default-store = "database"
|
||||||
|
|
||||||
|
# A list of possible file stores. Each entry must have a unique
|
||||||
|
# id. The `type` is one of: default-database, filesystem, s3.
|
||||||
|
#
|
||||||
|
# The enabled property serves currently to define target stores
|
||||||
|
# for te "copy files" task. All stores with enabled=false are
|
||||||
|
# removed from the list. The `default-store` must be enabled.
|
||||||
|
stores = {
|
||||||
|
database =
|
||||||
|
{ enabled = true
|
||||||
|
type = "default-database"
|
||||||
|
}
|
||||||
|
|
||||||
|
filesystem =
|
||||||
|
{ enabled = false
|
||||||
|
type = "file-system"
|
||||||
|
directory = "/some/directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
minio =
|
||||||
|
{ enabled = false
|
||||||
|
type = "s3"
|
||||||
|
endpoint = "http://localhost:9000"
|
||||||
|
access-key = "username"
|
||||||
|
secret-key = "password"
|
||||||
|
bucket = "docspell"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -24,12 +24,18 @@ import scodec.bits.ByteVector
|
|||||||
object ConfigFile {
|
object ConfigFile {
|
||||||
private[this] val unsafeLogger = docspell.logging.unsafeLogger
|
private[this] val unsafeLogger = docspell.logging.unsafeLogger
|
||||||
|
|
||||||
|
// IntelliJ is wrong, this is required
|
||||||
import Implicits._
|
import Implicits._
|
||||||
|
|
||||||
def loadConfig[F[_]: Async](args: List[String]): F[Config] = {
|
def loadConfig[F[_]: Async](args: List[String]): F[Config] = {
|
||||||
val logger = docspell.logging.getLogger[F]
|
val logger = docspell.logging.getLogger[F]
|
||||||
val validate =
|
val validate =
|
||||||
Validation.of(generateSecretIfEmpty, duplicateOpenIdProvider, signKeyVsUserUrl)
|
Validation.of(
|
||||||
|
generateSecretIfEmpty,
|
||||||
|
duplicateOpenIdProvider,
|
||||||
|
signKeyVsUserUrl,
|
||||||
|
filesValidate
|
||||||
|
)
|
||||||
ConfigFactory
|
ConfigFactory
|
||||||
.default[F, Config](logger, "docspell.server")(args, validate)
|
.default[F, Config](logger, "docspell.server")(args, validate)
|
||||||
}
|
}
|
||||||
@ -97,4 +103,7 @@ object ConfigFile {
|
|||||||
.map(checkProvider)
|
.map(checkProvider)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def filesValidate: Validation[Config] =
|
||||||
|
Validation(cfg => cfg.backend.files.validate.map(_ => cfg))
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import docspell.restserver.http4s.InternalHeader
|
|||||||
import docspell.restserver.ws.OutputEvent.KeepAlive
|
import docspell.restserver.ws.OutputEvent.KeepAlive
|
||||||
import docspell.restserver.ws.OutputEvent
|
import docspell.restserver.ws.OutputEvent
|
||||||
import docspell.store.Store
|
import docspell.store.Store
|
||||||
import docspell.store.file.FileRepositoryConfig
|
|
||||||
import docspell.store.records.RInternalSetting
|
import docspell.store.records.RInternalSetting
|
||||||
import org.http4s._
|
import org.http4s._
|
||||||
import org.http4s.blaze.client.BlazeClientBuilder
|
import org.http4s.blaze.client.BlazeClientBuilder
|
||||||
@ -74,7 +73,7 @@ object RestServer {
|
|||||||
httpClient <- BlazeClientBuilder[F].resource
|
httpClient <- BlazeClientBuilder[F].resource
|
||||||
store <- Store.create[F](
|
store <- Store.create[F](
|
||||||
cfg.backend.jdbc,
|
cfg.backend.jdbc,
|
||||||
FileRepositoryConfig.Database(cfg.backend.files.chunkSize),
|
cfg.backend.files.toFileRepositoryConfig,
|
||||||
pools.connectEC
|
pools.connectEC
|
||||||
)
|
)
|
||||||
setting <- Resource.eval(store.transact(RInternalSetting.create))
|
setting <- Resource.eval(store.transact(RInternalSetting.create))
|
||||||
|
Reference in New Issue
Block a user