From bfc60bb23f5b41320d2e357b798e1e161308b5d1 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 21 May 2024 11:24:47 +1000 Subject: [PATCH] Replace viper with koanf (#4845) * Migrate to koanf * Use temp logger for crashes before config is initialised * Remove snake case hacks * Add migration for config file keys * Add migration note for new migration * Renamed viper functions * Remove front-end viper workaround * Correctly default scan options --- cmd/stash/main.go | 12 + go.mod | 5 +- go.sum | 89 +++++ graphql/schema/schema.graphql | 16 +- internal/manager/config/config.go | 327 ++++++++++-------- .../manager/config/config_concurrency_test.go | 11 +- internal/manager/config/config_test.go | 34 ++ internal/manager/config/init.go | 135 +++++--- internal/manager/config/map.go | 117 ------- internal/manager/config/map_test.go | 82 ----- pkg/sqlite/database.go | 2 +- .../migrations/58_config_correct.up.sql | 1 + pkg/sqlite/migrations/58_postmigrate.go | 143 ++++++++ .../Settings/Tasks/LibraryTasks.tsx | 16 +- ui/v2.5/src/core/config.ts | 38 +- ui/v2.5/src/docs/en/MigrationNotes/58.md | 1 + ui/v2.5/src/docs/en/MigrationNotes/index.ts | 2 + 17 files changed, 594 insertions(+), 437 deletions(-) create mode 100644 internal/manager/config/config_test.go delete mode 100644 internal/manager/config/map.go delete mode 100644 internal/manager/config/map_test.go create mode 100644 pkg/sqlite/migrations/58_config_correct.up.sql create mode 100644 pkg/sqlite/migrations/58_postmigrate.go create mode 100644 ui/v2.5/src/docs/en/MigrationNotes/58.md diff --git a/cmd/stash/main.go b/cmd/stash/main.go index e37164171..d77786171 100644 --- a/cmd/stash/main.go +++ b/cmd/stash/main.go @@ -37,6 +37,8 @@ func main() { defer recoverPanic() + initLogTemp() + helpFlag := false pflag.BoolVarP(&helpFlag, "help", "h", false, "show this help text and exit") @@ -104,6 +106,16 @@ func main() { exitCode = <-exit } +// initLogTemp initializes a temporary logger for use before the config is loaded. +// Logs only error level message to stderr. +func initLogTemp() *log.Logger { + l := log.NewLogger() + l.Init("", true, "Error") + logger.Logger = l + + return l +} + func initLog(cfg *config.Config) *log.Logger { l := log.NewLogger() l.Init(cfg.GetLogFile(), cfg.GetLogOut(), cfg.GetLogLevel()) diff --git a/go.mod b/go.mod index 214e7494c..c47641c22 100644 --- a/go.mod +++ b/go.mod @@ -31,6 +31,7 @@ require ( github.com/json-iterator/go v1.1.12 github.com/kermieisinthehouse/gosx-notifier v0.1.2 github.com/kermieisinthehouse/systray v1.2.4 + github.com/knadh/koanf v1.5.0 github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-sqlite3 v1.14.17 github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007 @@ -40,7 +41,6 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cast v1.5.1 github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 github.com/tidwall/gjson v1.16.0 github.com/vearutop/statigz v1.4.0 @@ -86,8 +86,10 @@ require ( github.com/matryer/moq v0.2.3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect @@ -100,6 +102,7 @@ require ( github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cobra v1.7.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/viper v1.16.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tidwall/match v1.1.1 // indirect diff --git a/go.sum b/go.sum index 8964016e6..fc7a78ccc 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/anacrolix/dms v1.2.2 h1:0mk2/DXNqa5KDDbaLgFPf3oMV6VCGdFNh3d/gt4oafM= github.com/anacrolix/dms v1.2.2/go.mod h1:msPKAoppoNRfrYplJqx63FZ+VipDZ4Xsj3KzIQxyU7k= github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= @@ -98,6 +99,16 @@ github.com/asticode/go-astisub v0.25.1 h1:RZMGfZPp7CXOkI6g+zCU7DRLuciGPGup921uKZ github.com/asticode/go-astisub v0.25.1/go.mod h1:WTkuSzFB+Bp7wezuSf2Oxulj5A8zu2zLRVFf6bIFQK8= github.com/asticode/go-astits v1.8.0 h1:rf6aiiGn/QhlFjNON1n5plqF3Fs025XLUwiQ0NB6oZg= github.com/asticode/go-astits v1.8.0/go.mod h1:DkOWmBNQpnr9mv24KfZjq4JawCFX1FCqjLVGvO0DygQ= +github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4= +github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw= +github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ= +github.com/aws/aws-sdk-go-v2/service/appconfig v1.4.2/go.mod h1:FZ3HkCe+b10uFZZkFdvf98LHW21k49W8o8J366lqVKY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8= +github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk= +github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g= +github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -166,6 +177,7 @@ github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8 github.com/doug-martin/goqu/v9 v9.18.0 h1:/6bcuEtAe6nsSMVK/M+fOiXUNfyFF3yYtE07DBPFMYY= github.com/doug-martin/goqu/v9 v9.18.0/go.mod h1:nf0Wc2/hV3gYK9LiyqIrzBEVGlI8qW3GuDCEobC4wBQ= github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -180,7 +192,9 @@ github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= @@ -199,13 +213,17 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE= github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -258,6 +276,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -274,6 +293,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -312,9 +332,11 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/consul/api v1.13.0/go.mod h1:ZlVrynguJKcYr54zGaDbaL3fOvKC9m72FhPvA8T35KQ= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= @@ -322,6 +344,8 @@ github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= +github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -331,12 +355,17 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= @@ -352,6 +381,12 @@ github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hashicorp/vault/api v1.0.4/go.mod h1:gDcqh3WGcR1cpF5AJz/B1UFheUEneMoIospckxBxk6Q= +github.com/hashicorp/vault/sdk v0.1.13/go.mod h1:B+hVj7TpuQY1Y/GPbCpffmgd+tSEwvhkWnjtSYCaS2M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= +github.com/hjson/hjson-go/v4 v4.0.0 h1:wlm6IYYqHjOdXH1gHev4VoXCaW20HdQAGCxdOEEg2cs= +github.com/hjson/hjson-go/v4 v4.0.0/go.mod h1:KaYt3bTw3zhBjYqnXkYywcYctk0A2nxeEFTse3rH13E= github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -362,12 +397,18 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -375,6 +416,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kermieisinthehouse/gosx-notifier v0.1.2 h1:KV0KBeKK2B24kIHY7iK0jgS64Q05f4oB+hUZmsPodxQ= github.com/kermieisinthehouse/gosx-notifier v0.1.2/go.mod h1:xyWT07azFtUOcHl96qMVvKhvKzsMcS7rKTHQyv8WTho= github.com/kermieisinthehouse/systray v1.2.4 h1:pdH5vnl+KKjRrVCRU4g/2W1/0HVzuuJ6WXHlPPHYY6s= @@ -382,7 +424,10 @@ github.com/kermieisinthehouse/systray v1.2.4/go.mod h1:axh6C/jNuSyC0QGtidZJURc9h github.com/kevinmbeaulieu/eq-go v1.0.0/go.mod h1:G3S8ajA56gKBZm4UB9AOyoOS37JO3roToPzKNM8dtdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knadh/koanf v1.5.0 h1:q2TSd/3Pyc/5yP9ldIrSdIz26MCcyNQzW0pEAugLPNs= +github.com/knadh/koanf v1.5.0/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -434,16 +479,25 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.2.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -453,20 +507,26 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007 h1:Ohgj9L0EYOgXxkDp+bczlMBiulwmqYzQpvQNUdtt3oc= github.com/natefinch/pie v0.0.0-20170715172608-9a0d72014007/go.mod h1:wKCOWMb6iNlvKiOToY2cNuaovSXvIiv1zDi9QDR7aGQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= +github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -483,17 +543,24 @@ github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSg github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= +github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= @@ -509,6 +576,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/sagikazarmark/crypt v0.4.0/go.mod h1:ALv2SRj7GxYV4HO9elxH9nS6M9gW+xDNxqmyJ6RfDFM= @@ -520,6 +589,7 @@ github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5K github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -601,8 +671,11 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zencoder/go-dash/v3 v3.0.2 h1:oP1+dOh+Gp57PkvdCyMfbHtrHaxfl3w4kR3KBBbuqQE= github.com/zencoder/go-dash/v3 v3.0.2/go.mod h1:30R5bKy1aUYY45yesjtZ9l8trNc2TwNqbS17WVQmCzk= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -768,9 +841,11 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190415145633-3fd5a3612ccd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -782,10 +857,12 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -800,6 +877,8 @@ golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -807,6 +886,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -819,6 +899,7 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -858,6 +939,7 @@ golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -984,6 +1066,7 @@ google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1047,9 +1130,11 @@ google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ6 google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -1090,6 +1175,7 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1102,12 +1188,14 @@ gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.3/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -1124,3 +1212,4 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/graphql/schema/schema.graphql b/graphql/schema/schema.graphql index 2d7c57c0b..5ec16b17b 100644 --- a/graphql/schema/schema.graphql +++ b/graphql/schema/schema.graphql @@ -354,15 +354,19 @@ type Mutation { input: ConfigDefaultSettingsInput! ): ConfigDefaultSettingsResult! - # overwrites the entire plugin configuration for the given plugin + "overwrites the entire plugin configuration for the given plugin" configurePlugin(plugin_id: ID!, input: Map!): Map! - # overwrites the UI configuration - # if input is provided, then the entire UI configuration is replaced - # if partial is provided, then the partial UI configuration is merged into the existing UI configuration + """ + overwrites the UI configuration + if input is provided, then the entire UI configuration is replaced + if partial is provided, then the partial UI configuration is merged into the existing UI configuration + """ configureUI(input: Map, partial: Map): Map! - # sets a single UI key value - # key is a dot separated path to the value + """ + sets a single UI key value + key is a dot separated path to the value + """ configureUISetting(key: String!, value: Any): Map! "Generate and set (or clear) API key" diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 4b4bcd70e..9a65fd301 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "regexp" "runtime" "strconv" @@ -14,7 +15,9 @@ import ( "golang.org/x/crypto/bcrypt" - "github.com/spf13/viper" + "github.com/knadh/koanf" + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/file" "github.com/stashapp/stash/internal/identify" "github.com/stashapp/stash/pkg/fsutil" @@ -184,9 +187,9 @@ const ( autostartVideoOnPlaySelectedDefault = true ContinuePlaylistDefault = "continue_playlist_default" ShowStudioAsText = "show_studio_as_text" - CSSEnabled = "cssEnabled" - JavascriptEnabled = "javascriptEnabled" - CustomLocalesEnabled = "customLocalesEnabled" + CSSEnabled = "cssenabled" + JavascriptEnabled = "javascriptenabled" + CustomLocalesEnabled = "customlocalesenabled" ShowScrubber = "show_scrubber" showScrubberDefault = true @@ -242,12 +245,12 @@ const ( DLNAPortDefault = 1338 // Logging options - LogFile = "logFile" - LogOut = "logOut" + LogFile = "logfile" + LogOut = "logout" defaultLogOut = true - LogLevel = "logLevel" + LogLevel = "loglevel" defaultLogLevel = "Info" - LogAccess = "logAccess" + LogAccess = "logaccess" defaultLogAccess = true // Default settings @@ -261,7 +264,7 @@ const ( deleteGeneratedDefaultDefault = true // Desktop Integration Options - NoBrowser = "noBrowser" + NoBrowser = "nobrowser" NoBrowserDefault = false NotificationsEnabled = "notifications_enabled" NotificationsEnabledDefault = true @@ -283,6 +286,10 @@ var ( defaultMenuItems = []string{"scenes", "images", "movies", "markers", "galleries", "performers", "studios", "tags"} ) +var jsonUnmarshalConf = koanf.UnmarshalConf{ + Tag: "json", +} + type MissingConfigError struct { missingFields []string } @@ -303,12 +310,13 @@ func (s *StashBoxError) Error() string { type Config struct { // main instance - backed by config file - main *viper.Viper + main *koanf.Koanf // override instance - populated from flags/environment // not written to config file - overrides *viper.Viper + overrides *koanf.Koanf + filePath string isNewSystem bool // configUpdates chan int certFile string @@ -326,6 +334,15 @@ func GetInstance() *Config { return instance } +func (i *Config) load(f string) error { + if err := i.main.Load(file.Provider(f), yaml.Parser()); err != nil { + return err + } + + i.filePath = f + return nil +} + func (i *Config) IsNewSystem() bool { return i.isNewSystem } @@ -333,7 +350,7 @@ func (i *Config) IsNewSystem() bool { func (i *Config) SetConfigFile(fn string) { i.Lock() defer i.Unlock() - i.main.SetConfigFile(fn) + i.filePath = fn } func (i *Config) InitTLS() { @@ -381,13 +398,41 @@ func (i *Config) Set(key string, value interface{}) { // } i.Lock() defer i.Unlock() - i.main.Set(key, value) + + i.set(key, value) +} + +func (i *Config) set(key string, value interface{}) { + // assumes lock held + + // default behaviour for Set is to merge the value + // we want to replace it + i.main.Delete(key) + + if value == nil { + return + } + + // test for nil interface as well + refVal := reflect.ValueOf(value) + if refVal.Kind() == reflect.Ptr && refVal.IsNil() { + return + } + + _ = i.main.Set(key, value) } func (i *Config) SetDefault(key string, value interface{}) { i.Lock() defer i.Unlock() - i.main.SetDefault(key, value) + + i.setDefault(key, value) +} + +func (i *Config) setDefault(key string, value interface{}) { + if !i.main.Exists(key) { + i.set(key, value) + } } func (i *Config) SetPassword(value string) { @@ -402,7 +447,24 @@ func (i *Config) SetPassword(value string) { func (i *Config) Write() error { i.Lock() defer i.Unlock() - return i.main.WriteConfig() + + data, err := i.marshal() + if err != nil { + return err + } + + return os.WriteFile(i.filePath, data, 0644) +} + +func (i *Config) Marshal() ([]byte, error) { + i.RLock() + defer i.RUnlock() + + return i.marshal() +} + +func (i *Config) marshal() ([]byte, error) { + return i.main.Marshal(yaml.Parser()) } // FileEnvSet returns true if the configuration file environment parameter @@ -415,7 +477,7 @@ func FileEnvSet() bool { func (i *Config) GetConfigFile() string { i.RLock() defer i.RUnlock() - return i.main.ConfigFileUsed() + return i.filePath } // GetConfigPath returns the path of the directory containing the used @@ -430,12 +492,12 @@ func (i *Config) GetDefaultDatabaseFilePath() string { return filepath.Join(i.GetConfigPath(), "stash-go.sqlite") } -// viper returns the viper instance that should be used to get the provided +// forKey returns the Koanf instance that should be used to get the provided // key. Returns the overrides instance if the key exists there, otherwise it // returns the main instance. Assumes read lock held. -func (i *Config) viper(key string) *viper.Viper { +func (i *Config) forKey(key string) *koanf.Koanf { v := i.main - if i.overrides.IsSet(key) { + if i.overrides.Exists(key) { v = i.overrides } @@ -444,10 +506,10 @@ func (i *Config) viper(key string) *viper.Viper { // viper returns the viper instance that has the key set. Returns nil // if no instance has the key. Assumes read lock held. -func (i *Config) viperWith(key string) *viper.Viper { - v := i.viper(key) +func (i *Config) with(key string) *koanf.Koanf { + v := i.forKey(key) - if v.IsSet(key) { + if v.Exists(key) { return v } @@ -458,7 +520,7 @@ func (i *Config) HasOverride(key string) bool { i.RLock() defer i.RUnlock() - return i.overrides.IsSet(key) + return i.overrides.Exists(key) } // These functions wrap the equivalent viper functions, checking the override @@ -468,28 +530,28 @@ func (i *Config) unmarshalKey(key string, rawVal interface{}) error { i.RLock() defer i.RUnlock() - return i.viper(key).UnmarshalKey(key, rawVal) + return i.forKey(key).Unmarshal(key, rawVal) } func (i *Config) getStringSlice(key string) []string { i.RLock() defer i.RUnlock() - return i.viper(key).GetStringSlice(key) + return i.forKey(key).Strings(key) } func (i *Config) getString(key string) string { i.RLock() defer i.RUnlock() - return i.viper(key).GetString(key) + return i.forKey(key).String(key) } func (i *Config) getBool(key string) bool { i.RLock() defer i.RUnlock() - return i.viper(key).GetBool(key) + return i.forKey(key).Bool(key) } func (i *Config) getBoolDefault(key string, def bool) bool { @@ -497,9 +559,9 @@ func (i *Config) getBoolDefault(key string, def bool) bool { defer i.RUnlock() ret := def - v := i.viper(key) - if v.IsSet(key) { - ret = v.GetBool(key) + v := i.forKey(key) + if v.Exists(key) { + ret = v.Bool(key) } return ret } @@ -508,21 +570,21 @@ func (i *Config) getInt(key string) int { i.RLock() defer i.RUnlock() - return i.viper(key).GetInt(key) + return i.forKey(key).Int(key) } func (i *Config) getFloat64(key string) float64 { i.RLock() defer i.RUnlock() - return i.viper(key).GetFloat64(key) + return i.forKey(key).Float64(key) } func (i *Config) getStringMapString(key string) map[string]string { i.RLock() defer i.RUnlock() - ret := i.viper(key).GetStringMapString(key) + ret := i.forKey(key).StringMap(key) // GetStringMapString returns an empty map regardless of whether the // key exists or not. @@ -543,13 +605,13 @@ func (i *Config) GetStashPaths() StashConfigs { var ret StashConfigs v := i.main - if !v.IsSet(Stash) { + if !v.Exists(Stash) { v = i.overrides } - if err := v.UnmarshalKey(Stash, &ret); err != nil || len(ret) == 0 { + if err := v.Unmarshal(Stash, &ret); err != nil || len(ret) == 0 { // fallback to legacy format - ss := v.GetStringSlice(Stash) + ss := v.Strings(Stash) ret = nil for _, path := range ss { toAdd := &StashConfig{ @@ -650,7 +712,7 @@ func (i *Config) GetImageExcludes() []string { func (i *Config) GetVideoExtensions() []string { ret := i.getStringSlice(VideoExtensions) - if ret == nil { + if len(ret) == 0 { ret = defaultVideoExtensions } return ret @@ -658,7 +720,7 @@ func (i *Config) GetVideoExtensions() []string { func (i *Config) GetImageExtensions() []string { ret := i.getStringSlice(ImageExtensions) - if ret == nil { + if len(ret) == 0 { ret = defaultImageExtensions } return ret @@ -666,7 +728,7 @@ func (i *Config) GetImageExtensions() []string { func (i *Config) GetGalleryExtensions() []string { ret := i.getStringSlice(GalleryExtensions) - if ret == nil { + if len(ret) == 0 { ret = defaultGalleryExtensions } return ret @@ -772,16 +834,15 @@ func (i *Config) GetAllPluginConfiguration() map[string]map[string]interface{} { ret := make(map[string]map[string]interface{}) - sub := i.viper(PluginsSetting).GetStringMap(PluginsSetting) + v := i.forKey(PluginsSetting) + + sub := v.Cut(PluginsSetting) if sub == nil { return ret } - for plugin := range sub { - // HACK: viper changes map keys to case insensitive values, so the workaround is to - // convert map keys to snake case for storage - name := fromSnakeCase(plugin) - ret[name] = fromSnakeCaseMap(i.viper(PluginsSetting).GetStringMap(PluginsSettingPrefix + plugin)) + for plugin := range sub.Raw() { + ret[plugin] = sub.Cut(plugin).Raw() } return ret @@ -791,26 +852,20 @@ func (i *Config) GetPluginConfiguration(pluginID string) map[string]interface{} i.RLock() defer i.RUnlock() - key := PluginsSettingPrefix + toSnakeCase(pluginID) + key := PluginsSettingPrefix + pluginID - // HACK: viper changes map keys to case insensitive values, so the workaround is to - // convert map keys to snake case for storage - v := i.viper(key).GetStringMap(key) - - return fromSnakeCaseMap(v) + return i.forKey(key).Cut(key).Raw() } +// SetPluginConfiguration sets the configuration for a plugin. +// It will overwrite any existing configuration. func (i *Config) SetPluginConfiguration(pluginID string, v map[string]interface{}) { i.Lock() defer i.Unlock() - pluginID = toSnakeCase(pluginID) - key := PluginsSettingPrefix + pluginID - // HACK: viper changes map keys to case insensitive values, so the workaround is to - // convert map keys to snake case for storage - i.viper(key).Set(key, toSnakeCaseMap(v)) + i.set(key, v) } func (i *Config) GetDisabledPlugins() []string { @@ -1050,9 +1105,9 @@ func (i *Config) GetMaxSessionAge() int { defer i.RUnlock() ret := DefaultMaxSessionAge - v := i.viper(MaxSessionAge) - if v.IsSet(MaxSessionAge) { - ret = v.GetInt(MaxSessionAge) + v := i.forKey(MaxSessionAge) + if v.Exists(MaxSessionAge) { + ret = v.Int(MaxSessionAge) } return ret @@ -1076,9 +1131,9 @@ func (i *Config) GetUILocation() string { func (i *Config) GetMenuItems() []string { i.RLock() defer i.RUnlock() - v := i.viper(MenuItems) - if v.IsSet(MenuItems) { - return v.GetStringSlice(MenuItems) + v := i.forKey(MenuItems) + if v.Exists(MenuItems) { + return v.Strings(MenuItems) } return defaultMenuItems } @@ -1092,9 +1147,9 @@ func (i *Config) GetWallShowTitle() bool { defer i.RUnlock() ret := defaultWallShowTitle - v := i.viper(WallShowTitle) - if v.IsSet(WallShowTitle) { - ret = v.GetBool(WallShowTitle) + v := i.forKey(WallShowTitle) + if v.Exists(WallShowTitle) { + ret = v.Bool(WallShowTitle) } return ret } @@ -1108,9 +1163,9 @@ func (i *Config) GetWallPlayback() string { defer i.RUnlock() ret := defaultWallPlayback - v := i.viper(WallPlayback) - if v.IsSet(WallPlayback) { - ret = v.GetString(WallPlayback) + v := i.forKey(WallPlayback) + if v.Exists(WallPlayback) { + ret = v.String(WallPlayback) } return ret @@ -1144,14 +1199,14 @@ func (i *Config) getSlideshowDelay() int { // assume have lock ret := defaultImageLightboxSlideshowDelay - v := i.viper(ImageLightboxSlideshowDelay) - if v.IsSet(ImageLightboxSlideshowDelay) { - ret = v.GetInt(ImageLightboxSlideshowDelay) + v := i.forKey(ImageLightboxSlideshowDelay) + if v.Exists(ImageLightboxSlideshowDelay) { + ret = v.Int(ImageLightboxSlideshowDelay) } else { // fallback to old location - v := i.viper(legacyImageLightboxSlideshowDelay) - if v.IsSet(legacyImageLightboxSlideshowDelay) { - ret = v.GetInt(legacyImageLightboxSlideshowDelay) + v := i.forKey(legacyImageLightboxSlideshowDelay) + if v.Exists(legacyImageLightboxSlideshowDelay) { + ret = v.Int(legacyImageLightboxSlideshowDelay) } } @@ -1168,24 +1223,24 @@ func (i *Config) GetImageLightboxOptions() ConfigImageLightboxResult { SlideshowDelay: &delay, } - if v := i.viperWith(ImageLightboxDisplayModeKey); v != nil { - mode := ImageLightboxDisplayMode(v.GetString(ImageLightboxDisplayModeKey)) + if v := i.with(ImageLightboxDisplayModeKey); v != nil { + mode := ImageLightboxDisplayMode(v.String(ImageLightboxDisplayModeKey)) ret.DisplayMode = &mode } - if v := i.viperWith(ImageLightboxScaleUp); v != nil { - value := v.GetBool(ImageLightboxScaleUp) + if v := i.with(ImageLightboxScaleUp); v != nil { + value := v.Bool(ImageLightboxScaleUp) ret.ScaleUp = &value } - if v := i.viperWith(ImageLightboxResetZoomOnNav); v != nil { - value := v.GetBool(ImageLightboxResetZoomOnNav) + if v := i.with(ImageLightboxResetZoomOnNav); v != nil { + value := v.Bool(ImageLightboxResetZoomOnNav) ret.ResetZoomOnNav = &value } - if v := i.viperWith(ImageLightboxScrollModeKey); v != nil { - mode := ImageLightboxScrollMode(v.GetString(ImageLightboxScrollModeKey)) + if v := i.with(ImageLightboxScrollModeKey); v != nil { + mode := ImageLightboxScrollMode(v.String(ImageLightboxScrollModeKey)) ret.ScrollMode = &mode } - if v := i.viperWith(ImageLightboxScrollAttemptsBeforeChange); v != nil { - ret.ScrollAttemptsBeforeChange = v.GetInt(ImageLightboxScrollAttemptsBeforeChange) + if v := i.with(ImageLightboxScrollAttemptsBeforeChange); v != nil { + ret.ScrollAttemptsBeforeChange = v.Int(ImageLightboxScrollAttemptsBeforeChange) } return ret @@ -1204,20 +1259,14 @@ func (i *Config) GetUIConfiguration() map[string]interface{} { i.RLock() defer i.RUnlock() - // HACK: viper changes map keys to case insensitive values, so the workaround is to - // convert map keys to snake case for storage - v := i.viper(UI).GetStringMap(UI) - - return fromSnakeCaseMap(v) + return i.forKey(UI).Cut(UI).Raw() } func (i *Config) SetUIConfiguration(v map[string]interface{}) { i.Lock() defer i.Unlock() - // HACK: viper changes map keys to case insensitive values, so the workaround is to - // convert map keys to snake case for storage - i.viper(UI).Set(UI, toSnakeCaseMap(v)) + i.set(UI, v) } func (i *Config) GetCSSPath() string { @@ -1375,11 +1424,11 @@ func (i *Config) GetDeleteGeneratedDefault() bool { func (i *Config) GetDefaultIdentifySettings() *identify.Options { i.RLock() defer i.RUnlock() - v := i.viper(DefaultIdentifySettings) + v := i.forKey(DefaultIdentifySettings) - if v.IsSet(DefaultIdentifySettings) { + if v.Exists(DefaultIdentifySettings) && v.Get(DefaultIdentifySettings) != nil { var ret identify.Options - if err := v.UnmarshalKey(DefaultIdentifySettings, &ret); err != nil { + if err := v.UnmarshalWithConf(DefaultIdentifySettings, &ret, jsonUnmarshalConf); err != nil { return nil } return &ret @@ -1394,11 +1443,11 @@ func (i *Config) GetDefaultIdentifySettings() *identify.Options { func (i *Config) GetDefaultScanSettings() *ScanMetadataOptions { i.RLock() defer i.RUnlock() - v := i.viper(DefaultScanSettings) + v := i.forKey(DefaultScanSettings) - if v.IsSet(DefaultScanSettings) { + if v.Exists(DefaultScanSettings) && v.Get(DefaultScanSettings) != nil { var ret ScanMetadataOptions - if err := v.UnmarshalKey(DefaultScanSettings, &ret); err != nil { + if err := v.UnmarshalWithConf(DefaultScanSettings, &ret, jsonUnmarshalConf); err != nil { return nil } return &ret @@ -1413,11 +1462,11 @@ func (i *Config) GetDefaultScanSettings() *ScanMetadataOptions { func (i *Config) GetDefaultAutoTagSettings() *AutoTagMetadataOptions { i.RLock() defer i.RUnlock() - v := i.viper(DefaultAutoTagSettings) + v := i.forKey(DefaultAutoTagSettings) - if v.IsSet(DefaultAutoTagSettings) { + if v.Exists(DefaultAutoTagSettings) { var ret AutoTagMetadataOptions - if err := v.UnmarshalKey(DefaultAutoTagSettings, &ret); err != nil { + if err := v.Unmarshal(DefaultAutoTagSettings, &ret); err != nil { return nil } return &ret @@ -1432,11 +1481,11 @@ func (i *Config) GetDefaultAutoTagSettings() *AutoTagMetadataOptions { func (i *Config) GetDefaultGenerateSettings() *models.GenerateMetadataOptions { i.RLock() defer i.RUnlock() - v := i.viper(DefaultGenerateSettings) + v := i.forKey(DefaultGenerateSettings) - if v.IsSet(DefaultGenerateSettings) { + if v.Exists(DefaultGenerateSettings) { var ret models.GenerateMetadataOptions - if err := v.UnmarshalKey(DefaultGenerateSettings, &ret); err != nil { + if err := v.Unmarshal(DefaultGenerateSettings, &ret); err != nil { return nil } return &ret @@ -1543,9 +1592,9 @@ func (i *Config) GetMaxUploadSize() int64 { defer i.RUnlock() ret := int64(1024) - v := i.viper(MaxUploadSize) - if v.IsSet(MaxUploadSize) { - ret = v.GetInt64(MaxUploadSize) + v := i.forKey(MaxUploadSize) + if v.Exists(MaxUploadSize) { + ret = v.Int64(MaxUploadSize) } return ret << 20 } @@ -1645,7 +1694,7 @@ func (i *Config) Validate() error { var missingFields []string for _, p := range mandatoryPaths { - if !i.viper(p).IsSet(p) || i.viper(p).GetString(p) == "" { + if !i.forKey(p).Exists(p) || i.forKey(p).String(p) == "" { missingFields = append(missingFields, p) } } @@ -1656,7 +1705,7 @@ func (i *Config) Validate() error { } } - if i.GetBlobsStorage() == BlobStorageTypeFilesystem && i.viper(BlobsPath).GetString(BlobsPath) == "" { + if i.GetBlobsStorage() == BlobStorageTypeFilesystem && i.forKey(BlobsPath).String(BlobsPath) == "" { return MissingConfigError{ missingFields: []string{BlobsPath}, } @@ -1676,52 +1725,52 @@ func (i *Config) setDefaultValues() { // set the default host and port so that these are written to the config // file - i.main.SetDefault(Host, hostDefault) - i.main.SetDefault(Port, portDefault) + i.setDefault(Host, hostDefault) + i.setDefault(Port, portDefault) - i.main.SetDefault(ParallelTasks, parallelTasksDefault) - i.main.SetDefault(SequentialScanning, SequentialScanningDefault) - i.main.SetDefault(PreviewSegmentDuration, previewSegmentDurationDefault) - i.main.SetDefault(PreviewSegments, previewSegmentsDefault) - i.main.SetDefault(PreviewExcludeStart, previewExcludeStartDefault) - i.main.SetDefault(PreviewExcludeEnd, previewExcludeEndDefault) - i.main.SetDefault(PreviewAudio, previewAudioDefault) - i.main.SetDefault(SoundOnPreview, false) + i.setDefault(ParallelTasks, parallelTasksDefault) + i.setDefault(SequentialScanning, SequentialScanningDefault) + i.setDefault(PreviewSegmentDuration, previewSegmentDurationDefault) + i.setDefault(PreviewSegments, previewSegmentsDefault) + i.setDefault(PreviewExcludeStart, previewExcludeStartDefault) + i.setDefault(PreviewExcludeEnd, previewExcludeEndDefault) + i.setDefault(PreviewAudio, previewAudioDefault) + i.setDefault(SoundOnPreview, false) - i.main.SetDefault(ThemeColor, DefaultThemeColor) + i.setDefault(ThemeColor, DefaultThemeColor) - i.main.SetDefault(WriteImageThumbnails, writeImageThumbnailsDefault) - i.main.SetDefault(CreateImageClipsFromVideos, createImageClipsFromVideosDefault) + i.setDefault(WriteImageThumbnails, writeImageThumbnailsDefault) + i.setDefault(CreateImageClipsFromVideos, createImageClipsFromVideosDefault) - i.main.SetDefault(Database, defaultDatabaseFilePath) + i.setDefault(Database, defaultDatabaseFilePath) - i.main.SetDefault(dangerousAllowPublicWithoutAuth, dangerousAllowPublicWithoutAuthDefault) - i.main.SetDefault(SecurityTripwireAccessedFromPublicInternet, securityTripwireAccessedFromPublicInternetDefault) + i.setDefault(dangerousAllowPublicWithoutAuth, dangerousAllowPublicWithoutAuthDefault) + i.setDefault(SecurityTripwireAccessedFromPublicInternet, securityTripwireAccessedFromPublicInternetDefault) // Set generated to the metadata path for backwards compat - i.main.SetDefault(Generated, i.main.GetString(Metadata)) + i.setDefault(Generated, i.main.String(Metadata)) - i.main.SetDefault(NoBrowser, NoBrowserDefault) - i.main.SetDefault(NotificationsEnabled, NotificationsEnabledDefault) - i.main.SetDefault(ShowOneTimeMovedNotification, ShowOneTimeMovedNotificationDefault) + i.setDefault(NoBrowser, NoBrowserDefault) + i.setDefault(NotificationsEnabled, NotificationsEnabledDefault) + i.setDefault(ShowOneTimeMovedNotification, ShowOneTimeMovedNotificationDefault) // Set default scrapers and plugins paths - i.main.SetDefault(ScrapersPath, defaultScrapersPath) - i.main.SetDefault(PluginsPath, defaultPluginsPath) + i.setDefault(ScrapersPath, defaultScrapersPath) + i.setDefault(PluginsPath, defaultPluginsPath) // Set default gallery cover regex - i.main.SetDefault(GalleryCoverRegex, galleryCoverRegexDefault) + i.setDefault(GalleryCoverRegex, galleryCoverRegexDefault) // Set NoProxy default - i.main.SetDefault(NoProxy, noProxyDefault) + i.setDefault(NoProxy, noProxyDefault) // set default package sources - i.main.SetDefault(PluginPackageSources, []map[string]string{{ + i.setDefault(PluginPackageSources, []map[string]string{{ "name": sourceDefaultName, "url": pluginPackageSourcesDefault, "localpath": sourceDefaultPath, }}) - i.main.SetDefault(ScraperPackageSources, []map[string]string{{ + i.setDefault(ScraperPackageSources, []map[string]string{{ "name": sourceDefaultName, "url": scraperPackageSourcesDefault, "localpath": sourceDefaultPath, @@ -1737,13 +1786,13 @@ func (i *Config) setExistingSystemDefaults() { if !i.isNewSystem { // Existing systems as of the introduction of auto-browser open should retain existing // behavior and not start the browser automatically. - if !i.main.InConfig(NoBrowser) { - i.main.Set(NoBrowser, true) + if !i.main.Exists(NoBrowser) { + i.set(NoBrowser, true) } // Existing systems as of the introduction of the taskbar should inform users. - if !i.main.InConfig(ShowOneTimeMovedNotification) { - i.main.Set(ShowOneTimeMovedNotification, true) + if !i.main.Exists(ShowOneTimeMovedNotification) { + i.set(ShowOneTimeMovedNotification, true) } } } diff --git a/internal/manager/config/config_concurrency_test.go b/internal/manager/config/config_concurrency_test.go index 1ebe45f26..a11e12264 100644 --- a/internal/manager/config/config_concurrency_test.go +++ b/internal/manager/config/config_concurrency_test.go @@ -3,6 +3,7 @@ package config import ( "sync" "testing" + "time" ) // should be run with -race @@ -16,6 +17,7 @@ func TestConcurrentConfigAccess(t *testing.T) { wg.Add(1) go func(wk int) { for l := 0; l < loops; l++ { + start := time.Now() if err := i.SetInitialConfig(); err != nil { t.Errorf("Failure setting initial configuration in worker %v iteration %v: %v", wk, l, err) } @@ -34,8 +36,12 @@ func TestConcurrentConfigAccess(t *testing.T) { i.Set(Generated, i.GetGeneratedPath()) i.Set(Metadata, i.GetMetadataPath()) i.Set(Database, i.GetDatabasePath()) - i.Set(JWTSignKey, i.GetJWTSignKey()) - i.Set(SessionStoreKey, i.GetSessionStoreKey()) + + // these must be set as strings since the original values are also strings + // setting them as []byte will cause the returned string to be corrupted + i.Set(JWTSignKey, string(i.GetJWTSignKey())) + i.Set(SessionStoreKey, string(i.GetSessionStoreKey())) + i.GetDefaultScrapersPath() i.Set(Exclude, i.GetExcludes()) i.Set(ImageExclude, i.GetImageExcludes()) @@ -116,6 +122,7 @@ func TestConcurrentConfigAccess(t *testing.T) { i.Set(AutostartVideoOnPlaySelected, i.GetAutostartVideoOnPlaySelected()) i.Set(ContinuePlaylistDefault, i.GetContinuePlaylistDefault()) i.Set(PythonPath, i.GetPythonPath()) + t.Logf("Worker %v iteration %v took %v", wk, l, time.Since(start)) } wg.Done() }(k) diff --git a/internal/manager/config/config_test.go b/internal/manager/config/config_test.go new file mode 100644 index 000000000..1bfe009d4 --- /dev/null +++ b/internal/manager/config/config_test.go @@ -0,0 +1,34 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfig_GetAllPluginConfiguration(t *testing.T) { + i := InitializeEmpty() + + assert.Equal(t, i.GetAllPluginConfiguration(), map[string]map[string]interface{}{}) + + i.SetPluginConfiguration("plugin1", map[string]interface{}{"key1": "value1"}) + + assert.Equal(t, map[string]map[string]interface{}{ + "plugin1": {"key1": "value1"}, + }, i.GetAllPluginConfiguration()) + + i.SetPluginConfiguration("plugin2", map[string]interface{}{"key2": "value2"}) + + assert.Equal(t, map[string]map[string]interface{}{ + "plugin1": {"key1": "value1"}, + "plugin2": {"key2": "value2"}, + }, i.GetAllPluginConfiguration()) + + // ensure SetPluginConfiguration overwrites existing configuration + i.SetPluginConfiguration("plugin2", map[string]interface{}{"key3": "value3"}) + + assert.Equal(t, map[string]map[string]interface{}{ + "plugin1": {"key1": "value1"}, + "plugin2": {"key3": "value3"}, + }, i.GetAllPluginConfiguration()) +} diff --git a/internal/manager/config/init.go b/internal/manager/config/init.go index 3abf395f8..6a764019b 100644 --- a/internal/manager/config/init.go +++ b/internal/manager/config/init.go @@ -8,8 +8,10 @@ import ( "path/filepath" "strings" + "github.com/knadh/koanf" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/posflag" "github.com/spf13/pflag" - "github.com/spf13/viper" "github.com/stashapp/stash/pkg/fsutil" "github.com/stashapp/stash/pkg/logger" @@ -20,7 +22,30 @@ type flagStruct struct { nobrowser bool } -var flags flagStruct +var ( + flags flagStruct + + homeDir, _ = os.UserHomeDir() + + defaultConfigLocations = []string{ + "config.yml", + filepath.Join(homeDir, ".stash", "config.yml"), + } + + // map of env vars to config keys + envBinds = map[string]string{ + "host": Host, + "port": Port, + "external_host": ExternalHost, + "generated": Generated, + "metadata": Metadata, + "cache": Cache, + "stash": Stash, + "ui": UILocation, + } +) + +var errConfigNotFound = errors.New("config file not found") func init() { pflag.IP("host", net.IPv4(0, 0, 0, 0), "ip address for the host") @@ -33,8 +58,8 @@ func init() { // Called at startup func Initialize() (*Config, error) { cfg := &Config{ - main: viper.New(), - overrides: viper.New(), + main: koanf.New("."), + overrides: koanf.New("."), } cfg.initOverrides() @@ -77,54 +102,49 @@ func Initialize() (*Config, error) { // Called by tests to initialize an empty config func InitializeEmpty() *Config { cfg := &Config{ - main: viper.New(), - overrides: viper.New(), + main: koanf.New("."), + overrides: koanf.New("."), } instance = cfg return instance } -func bindEnv(v *viper.Viper, key ...string) { - if err := v.BindEnv(key...); err != nil { - panic(fmt.Sprintf("unable to set environment key (%v): %v", key, err)) +func (i *Config) loadFromCommandLine() { + v := i.overrides + + if err := v.Load(posflag.ProviderWithFlag(pflag.CommandLine, ".", v, func(f *pflag.Flag) (string, interface{}) { + // ignore flags that have not been changed + if !f.Changed { + return "", nil + } + + return f.Name, posflag.FlagVal(pflag.CommandLine, f) + }), nil); err != nil { + logger.Errorf("failed to load flags: %v", err) + } +} + +func (i *Config) loadFromEnv() { + v := i.overrides + + if err := v.Load(env.ProviderWithValue("STASH_", ".", func(key, value string) (string, interface{}) { + key = strings.ToLower(strings.TrimPrefix(key, "STASH_")) + if newKey, ok := envBinds[key]; ok { + return newKey, value + } + + return "", nil + }), nil); err != nil { + logger.Errorf("failed to load envs: %v", err) } } func (i *Config) initOverrides() { - v := i.overrides - - // replace dashes with underscores in the flag names - normalizeFn := pflag.CommandLine.GetNormalizeFunc() - pflag.CommandLine.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { - result := normalizeFn(f, name) - name = strings.ReplaceAll(string(result), "-", "_") - return pflag.NormalizedName(name) - }) - - if err := v.BindPFlags(pflag.CommandLine); err != nil { - logger.Infof("failed to bind flags: %v", err) - } - - v.SetEnvPrefix("stash") // will be uppercased automatically - bindEnv(v, "host") // STASH_HOST - bindEnv(v, "port") // STASH_PORT - bindEnv(v, "external_host") // STASH_EXTERNAL_HOST - bindEnv(v, "generated") // STASH_GENERATED - bindEnv(v, "metadata") // STASH_METADATA - bindEnv(v, "cache") // STASH_CACHE - bindEnv(v, "stash") // STASH_STASH - bindEnv(v, "ui_location", "STASH_UI") // STASH_UI + i.loadFromCommandLine() + i.loadFromEnv() } func (i *Config) initConfig() error { - v := i.main - - // The config file is called config. Leave off the file extension. - v.SetConfigName("config") - - v.AddConfigPath(".") // Look for config in the working directory - v.AddConfigPath(filepath.FromSlash("$HOME/.stash")) // Look for the config in the home directory - configFile := "" envConfigFile := os.Getenv("STASH_CONFIG_FILE") @@ -135,8 +155,6 @@ func (i *Config) initConfig() error { } if configFile != "" { - v.SetConfigFile(configFile) - // if file does not exist, assume it is a new system if exists, _ := fsutil.FileExists(configFile); !exists { i.isNewSystem = true @@ -150,18 +168,33 @@ func (i *Config) initConfig() error { } return nil + } else { + // load from provided config file + if err := i.loadFirstFromFiles([]string{configFile}); err != nil { + return err + } } - } + } else { + // load from default locations + if err := i.loadFirstFromFiles(defaultConfigLocations); err != nil { + if errors.Is(err, errConfigNotFound) { + i.isNewSystem = true + return nil + } - err := v.ReadInConfig() // Find and read the config file - // if not found, assume its a new system - var notFoundErr viper.ConfigFileNotFoundError - if errors.As(err, ¬FoundErr) { - i.isNewSystem = true - return nil - } else if err != nil { - return err + return err + } } return nil } + +func (i *Config) loadFirstFromFiles(f []string) error { + for _, ff := range f { + if exists, _ := fsutil.FileExists(ff); exists { + return i.load(ff) + } + } + + return errConfigNotFound +} diff --git a/internal/manager/config/map.go b/internal/manager/config/map.go deleted file mode 100644 index 289e6e5df..000000000 --- a/internal/manager/config/map.go +++ /dev/null @@ -1,117 +0,0 @@ -package config - -import ( - "bytes" - "unicode" - - "github.com/spf13/cast" -) - -// HACK: viper changes map keys to case insensitive values, so the workaround is to -// convert the map to use snake-case keys - -// toSnakeCase converts a string from camelCase to snake_case -// NOTE: a double capital will be converted in a way that will yield a different result -// when converted back to camel case. -// For example: someIDs => some_ids => someIds -func toSnakeCase(v string) string { - var buf bytes.Buffer - underscored := false - for i, c := range v { - if !underscored && unicode.IsUpper(c) && i > 0 { - buf.WriteByte('_') - underscored = true - } else { - underscored = false - } - - buf.WriteRune(unicode.ToLower(c)) - } - return buf.String() -} - -// fromSnakeCase converts a string from snake_case to camelCase -func fromSnakeCase(v string) string { - var buf bytes.Buffer - leadingUnderscore := true - capvar := false - for i, c := range v { - switch { - case c == '_' && !leadingUnderscore && i > 0: - capvar = true - case c == '_' && leadingUnderscore: - buf.WriteRune(c) - case capvar: - buf.WriteRune(unicode.ToUpper(c)) - capvar = false - default: - leadingUnderscore = false - buf.WriteRune(c) - } - } - return buf.String() -} - -// fromSnakeCaseMap recursively converts a map using snake_case keys to camelCase keys -func fromSnakeCaseMap(m map[string]interface{}) map[string]interface{} { - return fromSnakeCaseValue(m).(map[string]interface{}) -} - -func fromSnakeCaseValue(val interface{}) interface{} { - switch v := val.(type) { - case map[interface{}]interface{}: - ret := cast.ToStringMap(v) - for k, vv := range ret { - adjKey := fromSnakeCase(k) - ret[adjKey] = fromSnakeCaseValue(vv) - } - return ret - case map[string]interface{}: - ret := make(map[string]interface{}) - for k, vv := range v { - adjKey := fromSnakeCase(k) - ret[adjKey] = fromSnakeCaseValue(vv) - } - return ret - case []interface{}: - ret := make([]interface{}, len(v)) - for i, vv := range v { - ret[i] = fromSnakeCaseValue(vv) - } - return ret - default: - return v - } -} - -// toSnakeCaseMap recursively converts a map using camelCase keys to snake_case keys -func toSnakeCaseMap(m map[string]interface{}) map[string]interface{} { - return toSnakeCaseValue(m).(map[string]interface{}) -} - -func toSnakeCaseValue(val interface{}) interface{} { - switch v := val.(type) { - case map[interface{}]interface{}: - ret := cast.ToStringMap(v) - for k, vv := range ret { - adjKey := toSnakeCase(k) - ret[adjKey] = toSnakeCaseValue(vv) - } - return ret - case map[string]interface{}: - ret := make(map[string]interface{}) - for k, vv := range v { - adjKey := toSnakeCase(k) - ret[adjKey] = toSnakeCaseValue(vv) - } - return ret - case []interface{}: - ret := make([]interface{}, len(v)) - for i, vv := range v { - ret[i] = toSnakeCaseValue(vv) - } - return ret - default: - return v - } -} diff --git a/internal/manager/config/map_test.go b/internal/manager/config/map_test.go deleted file mode 100644 index 3c7da15b2..000000000 --- a/internal/manager/config/map_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package config - -import ( - "testing" -) - -func Test_toSnakeCase(t *testing.T) { - tests := []struct { - name string - v string - want string - }{ - { - "basic", - "basic", - "basic", - }, - { - "two words", - "twoWords", - "two_words", - }, - { - "three word value", - "threeWordValue", - "three_word_value", - }, - { - "snake case", - "snake_case", - "snake_case", - }, - { - "double capital", - "doubleCApital", - "double_capital", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := toSnakeCase(tt.v); got != tt.want { - t.Errorf("toSnakeCase() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_fromSnakeCase(t *testing.T) { - tests := []struct { - name string - v string - want string - }{ - { - "basic", - "basic", - "basic", - }, - { - "two words", - "two_words", - "twoWords", - }, - { - "three word value", - "three_word_value", - "threeWordValue", - }, - { - "camel case", - "camelCase", - "camelCase", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := fromSnakeCase(tt.v); got != tt.want { - t.Errorf("fromSnakeCase() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/sqlite/database.go b/pkg/sqlite/database.go index 02616cd07..90d3706a5 100644 --- a/pkg/sqlite/database.go +++ b/pkg/sqlite/database.go @@ -30,7 +30,7 @@ const ( dbConnTimeout = 30 ) -var appSchemaVersion uint = 57 +var appSchemaVersion uint = 58 //go:embed migrations/*.sql var migrationsBox embed.FS diff --git a/pkg/sqlite/migrations/58_config_correct.up.sql b/pkg/sqlite/migrations/58_config_correct.up.sql new file mode 100644 index 000000000..fdf3e9cde --- /dev/null +++ b/pkg/sqlite/migrations/58_config_correct.up.sql @@ -0,0 +1 @@ +-- no schema changes \ No newline at end of file diff --git a/pkg/sqlite/migrations/58_postmigrate.go b/pkg/sqlite/migrations/58_postmigrate.go new file mode 100644 index 000000000..0c19d13f1 --- /dev/null +++ b/pkg/sqlite/migrations/58_postmigrate.go @@ -0,0 +1,143 @@ +package migrations + +import ( + "bytes" + "context" + "fmt" + "os" + "time" + "unicode" + + "github.com/jmoiron/sqlx" + "github.com/spf13/cast" + "github.com/stashapp/stash/internal/manager/config" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/sqlite" +) + +type schema58Migrator struct { + migrator +} + +func post58(ctx context.Context, db *sqlx.DB) error { + logger.Info("Running post-migration for schema version 58") + + m := schema58Migrator{ + migrator: migrator{ + db: db, + }, + } + + return m.migrate() +} + +func (m *schema58Migrator) migrate() error { + if err := m.migrateConfig(); err != nil { + return fmt.Errorf("failed to migrate config: %w", err) + } + + return nil +} + +// fromSnakeCase converts a string from snake_case to camelCase +func (m *schema58Migrator) fromSnakeCase(v string) string { + var buf bytes.Buffer + leadingUnderscore := true + capvar := false + for i, c := range v { + switch { + case c == '_' && !leadingUnderscore && i > 0: + capvar = true + case c == '_' && leadingUnderscore: + buf.WriteRune(c) + case capvar: + buf.WriteRune(unicode.ToUpper(c)) + capvar = false + default: + leadingUnderscore = false + buf.WriteRune(c) + } + } + return buf.String() +} + +// fromSnakeCaseMap recursively converts a map using snake_case keys to camelCase keys +func (m *schema58Migrator) fromSnakeCaseMap(mm map[string]interface{}) map[string]interface{} { + return m.fromSnakeCaseValue(mm).(map[string]interface{}) +} + +func (m *schema58Migrator) fromSnakeCaseValue(val interface{}) interface{} { + switch v := val.(type) { + case map[interface{}]interface{}: + ret := cast.ToStringMap(v) + for k, vv := range ret { + adjKey := m.fromSnakeCase(k) + ret[adjKey] = m.fromSnakeCaseValue(vv) + } + return ret + case map[string]interface{}: + ret := make(map[string]interface{}) + for k, vv := range v { + adjKey := m.fromSnakeCase(k) + ret[adjKey] = m.fromSnakeCaseValue(vv) + } + return ret + case []interface{}: + ret := make([]interface{}, len(v)) + for i, vv := range v { + ret[i] = m.fromSnakeCaseValue(vv) + } + return ret + default: + return v + } +} + +func (m *schema58Migrator) migrateConfig() error { + c := config.GetInstance() + + orgPath := c.GetConfigFile() + + if orgPath == "" { + // no config file to migrate (usually in a test) + return nil + } + + // save a backup of the original config file + backupPath := fmt.Sprintf("%s.57.%s", orgPath, time.Now().Format("20060102_150405")) + + data, err := c.Marshal() + if err != nil { + return fmt.Errorf("failed to marshal backup config: %w", err) + } + + logger.Infof("Backing up config to %s", backupPath) + if err := os.WriteFile(backupPath, data, 0644); err != nil { + return fmt.Errorf("failed to write backup config: %w", err) + } + + // migrate the plugin and UI configs from snake_case to camelCase + ui := c.GetUIConfiguration() + if ui != nil { + ui = m.fromSnakeCaseMap(ui) + c.SetUIConfiguration(ui) + } + + plugins := c.GetAllPluginConfiguration() + newPlugins := make(map[string]interface{}) + for key, value := range plugins { + key = m.fromSnakeCase(key) + newPlugins[key] = m.fromSnakeCaseMap(value) + } + + c.Set(config.PluginsSetting, newPlugins) + if err := c.Write(); err != nil { + return fmt.Errorf("failed to write config: %w", err) + } + + return nil +} + +func init() { + sqlite.RegisterPostMigration(58, post58) +} diff --git a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx index a0a6d08da..1cab7cfb6 100644 --- a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx @@ -81,7 +81,21 @@ export const LibraryTasks: React.FC = () => { identify: false, }); - const [scanOptions, setScanOptions] = useState({}); + function getDefaultScanOptions(): GQL.ScanMetadataInput { + return { + scanGenerateCovers: true, + scanGeneratePreviews: false, + scanGenerateImagePreviews: false, + scanGenerateSprites: false, + scanGeneratePhashes: false, + scanGenerateThumbnails: false, + scanGenerateClipPreviews: false, + }; + } + + const [scanOptions, setScanOptions] = useState( + getDefaultScanOptions() + ); const [autoTagOptions, setAutoTagOptions] = useState({ performers: ["*"], diff --git a/ui/v2.5/src/core/config.ts b/ui/v2.5/src/core/config.ts index e71b22f7d..1e0d1030a 100644 --- a/ui/v2.5/src/core/config.ts +++ b/ui/v2.5/src/core/config.ts @@ -88,46 +88,10 @@ export interface IUIConfig { taskDefaults?: Record; } -interface ISavedFilterRowBroken extends ISavedFilterRow { - savedfilterid?: number; -} - -interface ICustomFilterBroken extends ICustomFilter { - sortby?: string; -} - -type FrontPageContentBroken = ISavedFilterRowBroken | ICustomFilterBroken; - -// #4128: deal with incorrectly insensitivised keys (sortBy and savedFilterId) export function getFrontPageContent( ui: IUIConfig | undefined ): FrontPageContent[] | undefined { - return (ui?.frontPageContent as FrontPageContentBroken[] | undefined)?.map( - (content) => { - switch (content.__typename) { - case "SavedFilter": - if (content.savedfilterid) { - return { - ...content, - savedFilterId: content.savedFilterId ?? content.savedfilterid, - savedfilterid: undefined, - }; - } - return content; - case "CustomFilter": - if (content.sortby) { - return { - ...content, - sortBy: content.sortBy ?? content.sortby, - sortby: undefined, - }; - } - return content; - default: - return content; - } - } - ); + return ui?.frontPageContent as FrontPageContent[] | undefined; } function recentlyReleased( diff --git a/ui/v2.5/src/docs/en/MigrationNotes/58.md b/ui/v2.5/src/docs/en/MigrationNotes/58.md new file mode 100644 index 000000000..3fd12a75b --- /dev/null +++ b/ui/v2.5/src/docs/en/MigrationNotes/58.md @@ -0,0 +1 @@ +This migration corrects the plugin and UI settings in `config.yml` to use `camelCase` instead of `snake_case`. As a result, the migrated file will not be compatible with previous versions of stash. A backup of the current `config.yml` will be created in the same directory with the name `config.yml.57.`. The exact filename is written to the log. \ No newline at end of file diff --git a/ui/v2.5/src/docs/en/MigrationNotes/index.ts b/ui/v2.5/src/docs/en/MigrationNotes/index.ts index f450e12a6..dd4f3fcd1 100644 --- a/ui/v2.5/src/docs/en/MigrationNotes/index.ts +++ b/ui/v2.5/src/docs/en/MigrationNotes/index.ts @@ -1,9 +1,11 @@ import migration32 from "./32.md"; import migration39 from "./39.md"; import migration48 from "./48.md"; +import migration58 from "./58.md"; export const migrationNotes: Record = { 32: migration32, 39: migration39, 48: migration48, + 58: migration58, };