aboutsummaryrefslogtreecommitdiff
path: root/lib/pleroma/config
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pleroma/config')
-rw-r--r--lib/pleroma/config/converter.ex195
-rw-r--r--lib/pleroma/config/deprecation_warnings.ex46
-rw-r--r--lib/pleroma/config/loader.ex70
-rw-r--r--lib/pleroma/config/oban.ex38
-rw-r--r--lib/pleroma/config/transfer_task.ex201
-rw-r--r--lib/pleroma/config/version.ex25
-rw-r--r--lib/pleroma/config/versioning.ex292
7 files changed, 597 insertions, 270 deletions
diff --git a/lib/pleroma/config/converter.ex b/lib/pleroma/config/converter.ex
new file mode 100644
index 000000000..86d7ea8e2
--- /dev/null
+++ b/lib/pleroma/config/converter.ex
@@ -0,0 +1,195 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.Converter do
+ @moduledoc """
+ Converts json structures into elixir structures and types and vice versa.
+ """
+ @spec to_elixir_types(boolean() | String.t() | map() | list()) :: term()
+ def to_elixir_types(%{"tuple" => [":args", args]}) when is_list(args) do
+ arguments =
+ Enum.map(args, fn arg ->
+ if String.contains?(arg, ["{", "}"]) do
+ {elem, []} = Code.eval_string(arg)
+ elem
+ else
+ to_elixir_types(arg)
+ end
+ end)
+
+ {:args, arguments}
+ end
+
+ def to_elixir_types(%{"tuple" => [":proxy_url", %{"tuple" => [type, host, port]}]}) do
+ {:proxy_url, {string_to_elixir_types!(type), parse_host(host), port}}
+ end
+
+ def to_elixir_types(%{"tuple" => [":partial_chain", entity]}) do
+ {partial_chain, []} =
+ entity
+ |> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
+ |> Code.eval_string()
+
+ {:partial_chain, partial_chain}
+ end
+
+ def to_elixir_types(%{"tuple" => entity}) do
+ Enum.reduce(entity, {}, &Tuple.append(&2, to_elixir_types(&1)))
+ end
+
+ def to_elixir_types(entity) when is_map(entity) do
+ Map.new(entity, fn {k, v} -> {to_elixir_types(k), to_elixir_types(v)} end)
+ end
+
+ def to_elixir_types(entity) when is_list(entity) do
+ Enum.map(entity, &to_elixir_types/1)
+ end
+
+ def to_elixir_types(entity) when is_binary(entity) do
+ entity
+ |> String.trim()
+ |> string_to_elixir_types!()
+ end
+
+ def to_elixir_types(entity), do: entity
+
+ defp parse_host("localhost"), do: :localhost
+
+ defp parse_host(host) do
+ charlist = to_charlist(host)
+
+ case :inet.parse_address(charlist) do
+ {:error, :einval} ->
+ charlist
+
+ {:ok, ip} ->
+ ip
+ end
+ end
+
+ @spec string_to_elixir_types!(String.t()) ::
+ atom() | Regex.t() | module() | String.t() | no_return()
+ def string_to_elixir_types!("~r" <> _pattern = regex) do
+ pattern =
+ ~r/^~r(?'delimiter'[\/|"'([{<]{1})(?'pattern'.+)[\/|"')\]}>]{1}(?'modifier'[uismxfU]*)/u
+
+ delimiters = ["/", "|", "\"", "'", {"(", ")"}, {"[", "]"}, {"{", "}"}, {"<", ">"}]
+
+ with %{"modifier" => modifier, "pattern" => pattern, "delimiter" => regex_delimiter} <-
+ Regex.named_captures(pattern, regex),
+ {:ok, {leading, closing}} <- find_valid_delimiter(delimiters, pattern, regex_delimiter),
+ {result, _} <- Code.eval_string("~r#{leading}#{pattern}#{closing}#{modifier}") do
+ result
+ end
+ end
+
+ def string_to_elixir_types!(":" <> atom), do: String.to_atom(atom)
+
+ def string_to_elixir_types!(value) do
+ if module_name?(value) do
+ String.to_existing_atom("Elixir." <> value)
+ else
+ value
+ end
+ end
+
+ defp find_valid_delimiter([], _string, _) do
+ raise(ArgumentError, message: "valid delimiter for Regex expression not found")
+ end
+
+ defp find_valid_delimiter([{leading, closing} = delimiter | others], pattern, regex_delimiter)
+ when is_tuple(delimiter) do
+ if String.contains?(pattern, closing) do
+ find_valid_delimiter(others, pattern, regex_delimiter)
+ else
+ {:ok, {leading, closing}}
+ end
+ end
+
+ defp find_valid_delimiter([delimiter | others], pattern, regex_delimiter) do
+ if String.contains?(pattern, delimiter) do
+ find_valid_delimiter(others, pattern, regex_delimiter)
+ else
+ {:ok, {delimiter, delimiter}}
+ end
+ end
+
+ @spec module_name?(String.t()) :: boolean()
+ def module_name?(string) do
+ Regex.match?(~r/^(Pleroma|Phoenix|Tesla|Quack|Ueberauth|Swoosh)\./, string) or
+ string in ["Oban", "Ueberauth", "ExSyslogger", "ConcurrentLimiter"]
+ end
+
+ @spec to_json_types(term()) :: map() | list() | boolean() | String.t() | integer()
+ def to_json_types(entity) when is_list(entity) do
+ Enum.map(entity, &to_json_types/1)
+ end
+
+ def to_json_types(%Regex{} = entity), do: inspect(entity)
+
+ def to_json_types(entity) when is_map(entity) do
+ Map.new(entity, fn {k, v} -> {to_json_types(k), to_json_types(v)} end)
+ end
+
+ def to_json_types({:args, args}) when is_list(args) do
+ arguments =
+ Enum.map(args, fn
+ arg when is_tuple(arg) -> inspect(arg)
+ arg -> to_json_types(arg)
+ end)
+
+ %{"tuple" => [":args", arguments]}
+ end
+
+ def to_json_types({:proxy_url, {type, :localhost, port}}) do
+ %{"tuple" => [":proxy_url", %{"tuple" => [to_json_types(type), "localhost", port]}]}
+ end
+
+ def to_json_types({:proxy_url, {type, host, port}}) when is_tuple(host) do
+ ip =
+ host
+ |> :inet_parse.ntoa()
+ |> to_string()
+
+ %{
+ "tuple" => [
+ ":proxy_url",
+ %{"tuple" => [to_json_types(type), ip, port]}
+ ]
+ }
+ end
+
+ def to_json_types({:proxy_url, {type, host, port}}) do
+ %{
+ "tuple" => [
+ ":proxy_url",
+ %{"tuple" => [to_json_types(type), to_string(host), port]}
+ ]
+ }
+ end
+
+ def to_json_types({:partial_chain, entity}),
+ do: %{"tuple" => [":partial_chain", inspect(entity)]}
+
+ def to_json_types(entity) when is_tuple(entity) do
+ value =
+ entity
+ |> Tuple.to_list()
+ |> to_json_types()
+
+ %{"tuple" => value}
+ end
+
+ def to_json_types(entity) when is_binary(entity), do: entity
+
+ def to_json_types(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity) do
+ entity
+ end
+
+ def to_json_types(entity) when entity in [:"tlsv1.1", :"tlsv1.2", :"tlsv1.3"] do
+ ":#{entity}"
+ end
+
+ def to_json_types(entity) when is_atom(entity), do: inspect(entity)
+end
diff --git a/lib/pleroma/config/deprecation_warnings.ex b/lib/pleroma/config/deprecation_warnings.ex
index 24aa5993b..19868d174 100644
--- a/lib/pleroma/config/deprecation_warnings.ex
+++ b/lib/pleroma/config/deprecation_warnings.ex
@@ -41,7 +41,8 @@ defmodule Pleroma.Config.DeprecationWarnings do
:ok <- check_gun_pool_options(),
:ok <- check_activity_expiration_config(),
:ok <- check_remote_ip_plug_name(),
- :ok <- check_uploders_s3_public_endpoint() do
+ :ok <- check_uploders_s3_public_endpoint(),
+ :ok <- check_oban_config() do
:ok
else
_ ->
@@ -79,7 +80,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
move_namespace_and_warn(@mrf_config_map, warning_preface)
end
- @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | nil
+ @spec move_namespace_and_warn([config_map()], String.t()) :: :ok | :error
def move_namespace_and_warn(config_map, warning_preface) do
warning =
Enum.reduce(config_map, "", fn
@@ -102,7 +103,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
- @spec check_media_proxy_whitelist_config() :: :ok | nil
+ @spec check_media_proxy_whitelist_config() :: :ok | :error
def check_media_proxy_whitelist_config do
whitelist = Config.get([:media_proxy, :whitelist])
@@ -163,7 +164,7 @@ defmodule Pleroma.Config.DeprecationWarnings do
end
end
- @spec check_activity_expiration_config() :: :ok | nil
+ @spec check_activity_expiration_config() :: :ok | :error
def check_activity_expiration_config do
warning_preface = """
!!!DEPRECATION WARNING!!!
@@ -215,4 +216,41 @@ defmodule Pleroma.Config.DeprecationWarnings do
:ok
end
end
+
+ @spec check_oban_config() :: :ok | :error
+ def check_oban_config do
+ oban_config = Config.get(Oban)
+
+ {crontab, changed?} =
+ [
+ Pleroma.Workers.Cron.StatsWorker,
+ Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker,
+ Pleroma.Workers.Cron.ClearOauthTokenWorker
+ ]
+ |> Enum.reduce({oban_config[:crontab], false}, fn removed_worker, {acc, changed?} ->
+ with acc when is_list(acc) <- acc,
+ setting when is_tuple(setting) <-
+ Enum.find(acc, fn {_, worker} -> worker == removed_worker end) do
+ """
+ !!!OBAN CONFIG WARNING!!!
+ You are using old workers in Oban crontab settings, which were removed.
+ Please, remove setting from crontab in your config file (prod.secret.exs): #{
+ inspect(setting)
+ }
+ """
+ |> Logger.warn()
+
+ {List.delete(acc, setting), true}
+ else
+ _ -> {acc, changed?}
+ end
+ end)
+
+ if changed? do
+ Config.put(Oban, Keyword.put(oban_config, :crontab, crontab))
+ :error
+ else
+ :ok
+ end
+ end
end
diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex
index b64d06707..69fd458c0 100644
--- a/lib/pleroma/config/loader.ex
+++ b/lib/pleroma/config/loader.ex
@@ -3,57 +3,73 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.Loader do
+ @reject_groups [
+ :postgrex,
+ :tesla,
+ :phoenix,
+ :tzdata,
+ :http_signatures,
+ :web_push_encryption,
+ :floki,
+ :pbkdf2_elixir
+ ]
+
@reject_keys [
Pleroma.Repo,
Pleroma.Web.Endpoint,
:env,
:configurable_from_database,
:database,
- :swarm
- ]
-
- @reject_groups [
- :postgrex,
- :tesla
+ :ecto_repos,
+ Pleroma.Gun,
+ Pleroma.ReverseProxy.Client,
+ Pleroma.Web.Auth.Authenticator
]
if Code.ensure_loaded?(Config.Reader) do
@reader Config.Reader
-
- def read(path), do: @reader.read!(path)
+ @config_header "import Config\r\n\r\n"
else
# support for Elixir less than 1.9
@reader Mix.Config
- def read(path) do
- path
- |> @reader.eval!()
- |> elem(0)
- end
+ @config_header "use Mix.Config\r\n\r\n"
end
- @spec read(Path.t()) :: keyword()
+ @spec read!(Path.t()) :: keyword()
+ def read!(path), do: @reader.read!(path)
@spec merge(keyword(), keyword()) :: keyword()
def merge(c1, c2), do: @reader.merge(c1, c2)
+ @spec config_header() :: String.t()
+ def config_header, do: @config_header
+
@spec default_config() :: keyword()
def default_config do
- "config/config.exs"
- |> read()
- |> filter()
- end
+ config =
+ "config/config.exs"
+ |> read!()
+ |> filter()
+
+ logger_config =
+ :logger
+ |> Application.get_all_env()
+ |> Enum.filter(fn {key, _} -> key in [:backends, :console, :ex_syslogger] end)
- defp filter(configs) do
- configs
- |> Keyword.keys()
- |> Enum.reduce([], &Keyword.put(&2, &1, filter_group(&1, configs)))
+ merge(config, logger: logger_config)
end
- @spec filter_group(atom(), keyword()) :: keyword()
- def filter_group(group, configs) do
- Enum.reject(configs[group], fn {key, _v} ->
- key in @reject_keys or group in @reject_groups or
- (group == :phoenix and key == :serve_endpoints)
+ @spec filter(keyword()) :: keyword()
+ def filter(configs) do
+ Enum.reduce(configs, [], fn
+ {group, _settings}, group_acc when group in @reject_groups ->
+ group_acc
+
+ {group, settings}, group_acc ->
+ Enum.reduce(settings, group_acc, fn
+ {key, _value}, acc when key in @reject_keys -> acc
+ setting, acc -> Keyword.update(acc, group, [setting], &Keyword.merge(&1, [setting]))
+ end)
end)
end
end
diff --git a/lib/pleroma/config/oban.ex b/lib/pleroma/config/oban.ex
deleted file mode 100644
index 3e63bca40..000000000
--- a/lib/pleroma/config/oban.ex
+++ /dev/null
@@ -1,38 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Config.Oban do
- require Logger
-
- def warn do
- oban_config = Pleroma.Config.get(Oban)
-
- crontab =
- [
- Pleroma.Workers.Cron.StatsWorker,
- Pleroma.Workers.Cron.PurgeExpiredActivitiesWorker,
- Pleroma.Workers.Cron.ClearOauthTokenWorker
- ]
- |> Enum.reduce(oban_config[:crontab], fn removed_worker, acc ->
- with acc when is_list(acc) <- acc,
- setting when is_tuple(setting) <-
- Enum.find(acc, fn {_, worker} -> worker == removed_worker end) do
- """
- !!!OBAN CONFIG WARNING!!!
- You are using old workers in Oban crontab settings, which were removed.
- Please, remove setting from crontab in your config file (prod.secret.exs): #{
- inspect(setting)
- }
- """
- |> Logger.warn()
-
- List.delete(acc, setting)
- else
- _ -> acc
- end
- end)
-
- Pleroma.Config.put(Oban, Keyword.put(oban_config, :crontab, crontab))
- end
-end
diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex
deleted file mode 100644
index aad45aab8..000000000
--- a/lib/pleroma/config/transfer_task.ex
+++ /dev/null
@@ -1,201 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Config.TransferTask do
- use Task
-
- alias Pleroma.Config
- alias Pleroma.ConfigDB
- alias Pleroma.Repo
-
- require Logger
-
- @type env() :: :test | :benchmark | :dev | :prod
-
- @reboot_time_keys [
- {:pleroma, :hackney_pools},
- {:pleroma, :chat},
- {:pleroma, Oban},
- {:pleroma, :rate_limit},
- {:pleroma, :markup},
- {:pleroma, :streamer},
- {:pleroma, :pools},
- {:pleroma, :connections_pool}
- ]
-
- @reboot_time_subkeys [
- {:pleroma, Pleroma.Captcha, [:seconds_valid]},
- {:pleroma, Pleroma.Upload, [:proxy_remote]},
- {:pleroma, :instance, [:upload_limit]},
- {:pleroma, :gopher, [:enabled]}
- ]
-
- def start_link(restart_pleroma? \\ true) do
- load_and_update_env([], restart_pleroma?)
- if Config.get(:env) == :test, do: Ecto.Adapters.SQL.Sandbox.checkin(Repo)
- :ignore
- end
-
- @spec load_and_update_env([ConfigDB.t()], boolean()) :: :ok
- def load_and_update_env(deleted_settings \\ [], restart_pleroma? \\ true) do
- with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do
- # We need to restart applications for loaded settings take effect
-
- {logger, other} =
- (Repo.all(ConfigDB) ++ deleted_settings)
- |> Enum.map(&merge_with_default/1)
- |> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end)
-
- logger
- |> Enum.sort()
- |> Enum.each(&configure/1)
-
- started_applications = Application.started_applications()
-
- # TODO: some problem with prometheus after restart!
- reject = [nil, :prometheus, :postgrex]
-
- reject =
- if restart_pleroma? do
- reject
- else
- [:pleroma | reject]
- end
-
- other
- |> Enum.map(&update/1)
- |> Enum.uniq()
- |> Enum.reject(&(&1 in reject))
- |> maybe_set_pleroma_last()
- |> Enum.each(&restart(started_applications, &1, Config.get(:env)))
-
- :ok
- else
- {:configurable, false} -> Restarter.Pleroma.rebooted()
- end
- end
-
- defp maybe_set_pleroma_last(apps) do
- # to be ensured that pleroma will be restarted last
- if :pleroma in apps do
- apps
- |> List.delete(:pleroma)
- |> List.insert_at(-1, :pleroma)
- else
- Restarter.Pleroma.rebooted()
- apps
- end
- end
-
- defp merge_with_default(%{group: group, key: key, value: value} = setting) do
- default = Config.Holder.default_config(group, key)
-
- merged =
- cond do
- Ecto.get_meta(setting, :state) == :deleted -> default
- can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value)
- true -> value
- end
-
- {group, key, value, merged}
- end
-
- # change logger configuration in runtime, without restart
- defp configure({:quack, key, _, merged}) do
- Logger.configure_backend(Quack.Logger, [{key, merged}])
- :ok = update_env(:quack, key, merged)
- end
-
- defp configure({_, :backends, _, merged}) do
- # removing current backends
- Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
-
- Enum.each(merged, &Logger.add_backend/1)
-
- :ok = update_env(:logger, :backends, merged)
- end
-
- defp configure({_, key, _, merged}) when key in [:console, :ex_syslogger] do
- merged =
- if key == :console do
- put_in(merged[:format], merged[:format] <> "\n")
- else
- merged
- end
-
- backend =
- if key == :ex_syslogger,
- do: {ExSyslogger, :ex_syslogger},
- else: key
-
- Logger.configure_backend(backend, merged)
- :ok = update_env(:logger, key, merged)
- end
-
- defp configure({_, key, _, merged}) do
- Logger.configure([{key, merged}])
- :ok = update_env(:logger, key, merged)
- end
-
- defp update({group, key, value, merged}) do
- try do
- :ok = update_env(group, key, merged)
-
- if group != :pleroma or pleroma_need_restart?(group, key, value), do: group
- rescue
- error ->
- error_msg =
- "updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{
- inspect(value)
- } error: #{inspect(error)}"
-
- Logger.warn(error_msg)
-
- nil
- end
- end
-
- defp update_env(group, key, nil), do: Application.delete_env(group, key)
- defp update_env(group, key, value), do: Application.put_env(group, key, value)
-
- @spec pleroma_need_restart?(atom(), atom(), any()) :: boolean()
- def pleroma_need_restart?(group, key, value) do
- group_and_key_need_reboot?(group, key) or group_and_subkey_need_reboot?(group, key, value)
- end
-
- defp group_and_key_need_reboot?(group, key) do
- Enum.any?(@reboot_time_keys, fn {g, k} -> g == group and k == key end)
- end
-
- defp group_and_subkey_need_reboot?(group, key, value) do
- Keyword.keyword?(value) and
- Enum.any?(@reboot_time_subkeys, fn {g, k, subkeys} ->
- g == group and k == key and
- Enum.any?(Keyword.keys(value), &(&1 in subkeys))
- end)
- end
-
- defp restart(_, :pleroma, env), do: Restarter.Pleroma.restart_after_boot(env)
-
- defp restart(started_applications, app, _) do
- with {^app, _, _} <- List.keyfind(started_applications, app, 0),
- :ok <- Application.stop(app) do
- :ok = Application.start(app)
- else
- nil ->
- Logger.warn("#{app} is not started.")
-
- error ->
- error
- |> inspect()
- |> Logger.warn()
- end
- end
-
- defp can_be_merged?(val1, val2) when is_list(val1) and is_list(val2) do
- Keyword.keyword?(val1) and Keyword.keyword?(val2)
- end
-
- defp can_be_merged?(_val1, _val2), do: false
-end
diff --git a/lib/pleroma/config/version.ex b/lib/pleroma/config/version.ex
new file mode 100644
index 000000000..2f66cc039
--- /dev/null
+++ b/lib/pleroma/config/version.ex
@@ -0,0 +1,25 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.Version do
+ @moduledoc """
+ IMPORTANT!!!
+ Before modifying records in the database directly, please read "Config versioning" in `docs/development/config_versioning.md`.
+ """
+
+ use Ecto.Schema
+
+ import Ecto.Query, only: [from: 2]
+
+ schema "config_versions" do
+ field(:backup, Pleroma.EctoType.Config.BinaryValue)
+ field(:current, :boolean, default: true)
+
+ timestamps()
+ end
+
+ def all do
+ from(v in __MODULE__, order_by: [desc: v.id]) |> Pleroma.Repo.all()
+ end
+end
diff --git a/lib/pleroma/config/versioning.ex b/lib/pleroma/config/versioning.ex
new file mode 100644
index 000000000..b997da1db
--- /dev/null
+++ b/lib/pleroma/config/versioning.ex
@@ -0,0 +1,292 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Config.Versioning do
+ @moduledoc """
+ Module that manages versions of database configs.
+ """
+
+ import Ecto.Query, only: [from: 2]
+
+ alias Ecto.Multi
+ alias Pleroma.Config.Version
+ alias Pleroma.ConfigDB
+ alias Pleroma.Repo
+
+ @type change :: %{
+ optional(:delete) => boolean(),
+ optional(:value) => any(),
+ group: atom(),
+ key: atom() | nil
+ }
+
+ @doc """
+ Creates new config version:
+ - convert changes to elixir types
+ - splits changes by type and processes them in `config` table
+ - sets all pointers to false
+ - gets all rows from `config` table and inserts them as keyword in `backup` field
+ """
+ @spec new_version([change()] | change()) ::
+ {:ok, map()} | {:error, :no_changes} | {:error, atom() | tuple(), any(), any()}
+ def new_version([]), do: {:error, :empty_changes}
+ def new_version(change) when is_map(change), do: new_version([change])
+
+ def new_version(changes) when is_list(changes) do
+ changes
+ |> Enum.reduce(Multi.new(), fn
+ %{delete: true} = deletion, acc ->
+ Multi.run(acc, {:delete_or_update, deletion[:group], deletion[:key]}, fn _, _ ->
+ ConfigDB.delete_or_update(deletion)
+ end)
+
+ operation, acc ->
+ {name, fun} =
+ if Keyword.keyword?(operation[:value]) or
+ (operation[:group] == :pleroma and
+ operation[:key] in ConfigDB.pleroma_not_keyword_values()) do
+ {:insert_or_update,
+ fn _, _ ->
+ ConfigDB.update_or_create(operation)
+ end}
+ else
+ {:error,
+ fn _, _ ->
+ {:error, {:value_must_be_keyword, operation}}
+ end}
+ end
+
+ Multi.run(acc, {name, operation[:group], operation[:key]}, fun)
+ end)
+ |> set_current_flag_false_for_all_versions()
+ |> insert_new_version()
+ |> Repo.transaction()
+ end
+
+ def new_version(_), do: {:error, :bad_format}
+
+ defp set_current_flag_false_for_all_versions(multi) do
+ Multi.update_all(multi, :update_all_versions, Version, set: [current: false])
+ end
+
+ defp insert_new_version(multi) do
+ Multi.run(multi, :insert_version, fn repo, _ ->
+ %Version{
+ backup: ConfigDB.all_as_keyword()
+ }
+ |> repo.insert()
+ end)
+ end
+
+ @doc """
+ Rollbacks config version by N steps:
+ - checks possibility for rollback
+ - truncates config table and restarts pk
+ - inserts config settings from backup
+ - sets all pointers to false
+ - sets current pointer to true for rollback version
+ - deletes versions after current
+ """
+ @spec rollback(pos_integer()) ::
+ {:ok, map()}
+ | {:error, atom() | tuple(), any(), any()}
+ | {:error, :steps_format}
+ | {:error, :no_current_version}
+ | {:error, :rollback_not_possible}
+ def rollback(steps \\ 1)
+
+ def rollback(steps) when is_integer(steps) and steps > 0 do
+ with version_id when is_integer(version_id) <- get_current_version_id(),
+ %Version{} = version <- get_version_by_steps(steps) do
+ do_rollback(version)
+ end
+ end
+
+ def rollback(_), do: {:error, :steps_format}
+
+ @doc """
+ Same as `rollback/1`, but rollbacks for a given version id.
+ """
+ @spec rollback_by_id(pos_integer()) ::
+ {:ok, map()}
+ | {:error, atom() | tuple(), any(), any()}
+ | {:error, :not_found}
+ | {:error, :version_is_already_current}
+ def rollback_by_id(id) when is_integer(id) do
+ with %Version{current: false} = version <- get_version_by_id(id) do
+ do_rollback(version)
+ else
+ %Version{current: true} -> {:error, :version_is_already_current}
+ error -> error
+ end
+ end
+
+ defp get_current_version_id do
+ query = from(v in Version, where: v.current == true)
+
+ with nil <- Repo.aggregate(query, :max, :id) do
+ {:error, :no_current_version}
+ end
+ end
+
+ defp get_version_by_id(id) do
+ with nil <- Repo.get(Version, id) do
+ {:error, :not_found}
+ end
+ end
+
+ defp get_version_by_steps(steps) do
+ query = from(v in Version, order_by: [desc: v.id], limit: 1, offset: ^steps)
+
+ with nil <- Repo.one(query) do
+ {:error, :rollback_not_possible}
+ end
+ end
+
+ defp do_rollback(version) do
+ multi =
+ truncate_config_table()
+ |> reset_pk_in_config_table()
+
+ version.backup
+ |> ConfigDB.from_keyword_to_maps()
+ |> add_insert_commands(multi)
+ |> set_current_flag_false_for_all_versions()
+ |> Multi.update(:move_current_pointer, Ecto.Changeset.change(version, current: true))
+ |> Multi.delete_all(
+ :delete_next_versions,
+ from(v in Version, where: v.id > ^version.id)
+ )
+ |> Repo.transaction()
+ end
+
+ defp truncate_config_table(multi \\ Multi.new()) do
+ Multi.run(multi, :truncate_config_table, fn repo, _ ->
+ repo.query("TRUNCATE config;")
+ end)
+ end
+
+ defp reset_pk_in_config_table(multi) do
+ Multi.run(multi, :reset_pk, fn repo, _ ->
+ repo.query("ALTER SEQUENCE config_id_seq RESTART;")
+ end)
+ end
+
+ defp add_insert_commands(changes, multi) do
+ Enum.reduce(changes, multi, fn change, acc ->
+ Multi.run(acc, {:insert, change[:group], change[:key]}, fn _, _ ->
+ ConfigDB.update_or_create(change)
+ end)
+ end)
+ end
+
+ @doc """
+ Resets config table and creates new empty version.
+ """
+ @spec reset() :: {:ok, map()} | {:error, atom() | tuple(), any(), any()}
+ def reset do
+ truncate_config_table()
+ |> reset_pk_in_config_table()
+ |> set_current_flag_false_for_all_versions()
+ |> insert_new_version()
+ |> Repo.transaction()
+ end
+
+ @doc """
+ Migrates settings from config file into database:
+ - truncates config table and restarts pk
+ - inserts settings from config file
+ - sets all pointers to false
+ - gets all rows from `config` table and inserts them as keyword in `backup` field
+ """
+ @spec migrate(Path.t()) :: {:ok, map()} | {:error, atom() | tuple(), any(), any()}
+ def migrate(config_path) do
+ multi =
+ truncate_config_table()
+ |> reset_pk_in_config_table()
+
+ config_path
+ |> Pleroma.Config.Loader.read!()
+ |> Pleroma.Config.Loader.filter()
+ |> ConfigDB.from_keyword_to_maps()
+ |> add_insert_commands(multi)
+ |> set_current_flag_false_for_all_versions()
+ |> insert_new_version()
+ |> Repo.transaction()
+ end
+
+ @doc """
+ Common function to migrate old config namespace to the new one keeping the old value.
+ """
+ @spec migrate_namespace({atom(), atom()}, {atom(), atom()}) ::
+ {:ok, map()} | {:error, atom() | tuple(), any(), any()}
+ def migrate_namespace({o_group, o_key}, {n_group, n_key}) do
+ config = ConfigDB.get_by_params(%{group: o_group, key: o_key})
+
+ configs_changes_fun =
+ if config do
+ fn ->
+ config
+ |> Ecto.Changeset.change(group: n_group, key: n_key)
+ |> Repo.update()
+ end
+ else
+ fn -> {:ok, nil} end
+ end
+
+ versions_changes_fun = fn %{backup: backup} = version ->
+ with {value, rest} when not is_nil(value) <- pop_in(backup[o_group][o_key]) do
+ rest =
+ if rest[o_group] == [] do
+ Keyword.delete(rest, o_group)
+ else
+ rest
+ end
+
+ updated_backup =
+ if Keyword.has_key?(rest, n_group) do
+ put_in(rest[n_group][n_key], value)
+ else
+ Keyword.put(rest, n_group, [{n_key, value}])
+ end
+
+ version
+ |> Ecto.Changeset.change(backup: updated_backup)
+ |> Repo.update()
+ else
+ _ -> {:ok, nil}
+ end
+ end
+
+ migrate_configs_and_versions(configs_changes_fun, versions_changes_fun)
+ end
+
+ @doc """
+ Abstract function for config migrations to keep changes in config table and changes in versions backups in transaction.
+ Accepts two functions:
+ - first function makes changes to the configs
+ - second function makes changes to the backups in versions
+ """
+ @spec migrate_configs_and_versions(function(), function()) ::
+ {:ok, map()} | {:error, atom() | tuple(), any(), any()}
+ def migrate_configs_and_versions(configs_changes_fun, version_change_fun)
+ when is_function(configs_changes_fun, 0) and
+ is_function(version_change_fun, 1) do
+ versions = Repo.all(Version)
+
+ multi =
+ Multi.new()
+ |> Multi.run(:configs_changes, fn _, _ ->
+ configs_changes_fun.()
+ end)
+
+ versions
+ |> Enum.reduce(multi, fn version, acc ->
+ Multi.run(acc, {:version_change, version.id}, fn _, _ ->
+ version_change_fun.(version)
+ end)
+ end)
+ |> Repo.transaction()
+ end
+end